Create multiple tasks when uploading multiple videos (#4824)

* add alert when uploading photo and video together

* add creating multiple tasks for My computer tabs

* add creating multiple tasks for Remote source tab

* add creating multi tasks for File share tab

* add libmagic in dockerfile

* fix lint

* change class name of create multi tasks button

* fix incorrect deletion of validation error notification

* add opportunity upload manifest.jsonl with image files for single task create

* remove status showing of task from multitasks case

* refactoring create queue in mutlitasks case

* fix warning

* revert incorrect remove notification about error

* fix showing error of clone the repository

* fix esling error

* move of initialValue creating for task name

* rename isMultiTask properti to many

* return  incorrect deleted progress value

* add source and license on icon file

* fix unhandled promise rejection

* change mime_type getter method

* add hint how to see these template rules

* refactoring of multi task progress markup

* remove unnecessary processings for share

* remove unnecessary notification

* remove opportunity upload no video on multi mode

* correct formation of the task name

* rename function

* change queueSize to 1

* fix root selecting on share tab

* refactoring selectCloudStorageFiles logic

* add debig info. temporarily

* Some fixes

* Tried to fix unstable test

* Adjusted messages

* Fixed license headers

Co-authored-by: Boris <sekachev.bs@gmail.com>
main
Aleksey Alekseev 4 years ago committed by GitHub
parent 9f89787f95
commit 1db247aedd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -28,16 +29,16 @@ const shareActions = {
export type ShareActions = ActionUnion<typeof shareActions>;
export function loadShareDataAsync(directory: string, success: () => void, failure: () => void): ThunkAction {
return async (dispatch): Promise<void> => {
export function loadShareDataAsync(directory: string): ThunkAction {
return async (dispatch): Promise<ShareFileInfo[]> => {
try {
dispatch(shareActions.loadShareData());
const values = await core.server.share(directory);
success();
dispatch(shareActions.loadShareDataSuccess(values as ShareFileInfo[], directory));
return (values as ShareFileInfo[]);
} catch (error) {
failure();
dispatch(shareActions.loadShareDataFailed(error));
throw error;
}
};
}

@ -20,9 +20,6 @@ export enum TasksActionTypes {
DELETE_TASK = 'DELETE_TASK',
DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS',
DELETE_TASK_FAILED = 'DELETE_TASK_FAILED',
CREATE_TASK = 'CREATE_TASK',
CREATE_TASK_STATUS_UPDATED = 'CREATE_TASK_STATUS_UPDATED',
CREATE_TASK_SUCCESS = 'CREATE_TASK_SUCCESS',
CREATE_TASK_FAILED = 'CREATE_TASK_FAILED',
UPDATE_TASK = 'UPDATE_TASK',
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS',
@ -144,26 +141,6 @@ export function deleteTaskAsync(taskInstance: any): ThunkAction<Promise<void>, {
};
}
function createTask(): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK,
payload: {},
};
return action;
}
function createTaskSuccess(taskId: number): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK_SUCCESS,
payload: {
taskId,
},
};
return action;
}
function createTaskFailed(error: any): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK_FAILED,
@ -175,19 +152,9 @@ function createTaskFailed(error: any): AnyAction {
return action;
}
function createTaskUpdateStatus(status: string): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK_STATUS_UPDATED,
payload: {
status,
},
};
return action;
}
export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<any> => {
export function createTaskAsync(data: any, onProgress?: (status: string) => void):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch): Promise<any> => {
const description: any = {
name: data.basic.name,
labels: data.labels,
@ -246,7 +213,7 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
if (gitPlugin) {
gitPlugin.callbacks.onStatusChange = (status: string): void => {
dispatch(createTaskUpdateStatus(status));
onProgress?.(status);
};
gitPlugin.data.task = taskInstance;
gitPlugin.data.repos = data.advanced.repository;
@ -255,12 +222,10 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
}
}
dispatch(createTask());
try {
const savedTask = await taskInstance.save((status: string, progress: number): void => {
dispatch(createTaskUpdateStatus(status + (progress !== null ? ` ${Math.floor(progress * 100)}%` : '')));
onProgress?.(status + (progress !== null ? ` ${Math.floor(progress * 100)}%` : ''));
});
dispatch(createTaskSuccess(savedTask.id));
return savedTask;
} catch (error) {
dispatch(createTaskFailed(error));

@ -0,0 +1,5 @@
<!-- The icon received from: https://github.com/Templarian/MaterialDesign/blob/master/svg/plus-circle-multiple-outline.svg -->
<!-- License: Apache 2.0 -->
<svg width="20" height="15" viewBox="0 0 20 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6667 10.8333H13.3333V8.33333H15.8333V6.66667H13.3333V4.16667H11.6667V6.66667H9.16667V8.33333H11.6667V10.8333ZM5 14.5833C3.48611 14.0556 2.27444 13.1422 1.365 11.8433C0.455 10.545 0 9.09722 0 7.5C0 5.90278 0.455 4.45472 1.365 3.15583C2.27444 1.8575 3.48611 0.944444 5 0.416667V2.25C3.97222 2.73611 3.15972 3.45139 2.5625 4.39583C1.96528 5.34028 1.66667 6.375 1.66667 7.5C1.66667 8.625 1.96528 9.65972 2.5625 10.6042C3.15972 11.5486 3.97222 12.2639 5 12.75V14.5833ZM12.5 15C11.4583 15 10.4828 14.8022 9.57333 14.4067C8.66333 14.0106 7.87167 13.4756 7.19833 12.8017C6.52444 12.1283 5.98972 11.3367 5.59417 10.4267C5.19805 9.51722 5 8.54167 5 7.5C5 6.45833 5.19805 5.4825 5.59417 4.5725C5.98972 3.66306 6.52444 2.87139 7.19833 2.1975C7.87167 1.52417 8.66333 0.989444 9.57333 0.593333C10.4828 0.197778 11.4583 0 12.5 0C13.5417 0 14.5175 0.197778 15.4275 0.593333C16.3369 0.989444 17.1286 1.52417 17.8025 2.1975C18.4758 2.87139 19.0106 3.66306 19.4067 4.5725C19.8022 5.4825 20 6.45833 20 7.5C20 8.54167 19.8022 9.51722 19.4067 10.4267C19.0106 11.3367 18.4758 12.1283 17.8025 12.8017C17.1286 13.4756 16.3369 14.0106 15.4275 14.4067C14.5175 14.8022 13.5417 15 12.5 15ZM12.5 13.3333C14.125 13.3333 15.5033 12.7672 16.635 11.635C17.7672 10.5033 18.3333 9.125 18.3333 7.5C18.3333 5.875 17.7672 4.49639 16.635 3.36417C15.5033 2.2325 14.125 1.66667 12.5 1.66667C10.875 1.66667 9.49667 2.2325 8.365 3.36417C7.23278 4.49639 6.66667 5.875 6.66667 7.5C6.66667 9.125 7.23278 10.5033 8.365 11.635C9.49667 12.7672 10.875 13.3333 12.5 13.3333Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -1,37 +1,56 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React, { RefObject } from 'react';
import Input from 'antd/lib/input';
import Text from 'antd/lib/typography/Text';
import Tooltip from 'antd/lib/tooltip';
import Form, { FormInstance } from 'antd/lib/form';
import { Store } from 'antd/lib/form/interface';
import { QuestionCircleOutlined } from '@ant-design/icons';
export interface BaseConfiguration {
name: string;
}
interface Props {
onSubmit(values: BaseConfiguration): void;
onChange(values: BaseConfiguration): void;
many: boolean;
exampleMultiTaskName?: string;
}
export default class BasicConfigurationForm extends React.PureComponent<Props> {
private formRef: RefObject<FormInstance>;
private inputRef: RefObject<Input>;
private initialName: string;
public constructor(props: Props) {
super(props);
this.formRef = React.createRef<FormInstance>();
this.inputRef = React.createRef<Input>();
const { many } = this.props;
this.initialName = many ? '{{file_name}}' : '';
}
componentDidMount(): void {
const { onChange } = this.props;
onChange({
name: this.initialName,
});
}
private handleChangeName(e: React.ChangeEvent<HTMLInputElement>): void {
const { onChange } = this.props;
onChange({
name: e.target.value,
});
}
public submit(): Promise<void> {
const { onSubmit } = this.props;
if (this.formRef.current) {
return this.formRef.current.validateFields().then((values: Store): Promise<void> => {
onSubmit({ name: values.name });
return Promise.resolve();
});
return this.formRef.current.validateFields();
}
return Promise.reject(new Error('Form ref is empty'));
@ -50,6 +69,8 @@ export default class BasicConfigurationForm extends React.PureComponent<Props> {
}
public render(): JSX.Element {
const { many, exampleMultiTaskName } = this.props;
return (
<Form ref={this.formRef} layout='vertical'>
<Form.Item
@ -62,9 +83,48 @@ export default class BasicConfigurationForm extends React.PureComponent<Props> {
message: 'Task name cannot be empty',
},
]}
initialValue={this.initialName}
>
<Input ref={this.inputRef} />
<Input
ref={this.inputRef}
onChange={(e) => this.handleChangeName(e)}
/>
</Form.Item>
{many ? (
<Text type='secondary'>
<Tooltip title={() => (
<>
You can substitute in the template:
<ul>
<li>
some_text - any text
</li>
<li>
{'{{'}
index
{'}}'}
&nbsp;- index file in set
</li>
<li>
{'{{'}
file_name
{'}}'}
&nbsp;- name of file
</li>
</ul>
Example:&nbsp;
<i>
{exampleMultiTaskName || 'Task name 1 - video_1.mp4'}
</i>
</>
)}
>
When forming the name, a template is used.
{' '}
<QuestionCircleOutlined />
</Tooltip>
</Text>
) : null}
</Form>
);
}

@ -7,11 +7,11 @@ import React, { RefObject } from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import { Row, Col } from 'antd/lib/grid';
import Alert from 'antd/lib/alert';
import Button from 'antd/lib/button';
import Collapse from 'antd/lib/collapse';
import notification from 'antd/lib/notification';
import Text from 'antd/lib/typography/Text';
import Alert from 'antd/lib/alert';
// eslint-disable-next-line import/no-extraneous-dependencies
import { ValidateErrorEntity } from 'rc-field-form/lib/interface';
import { StorageLocation } from 'reducers';
@ -19,11 +19,20 @@ import { getCore, Storage } from 'cvat-core-wrapper';
import ConnectedFileManager from 'containers/file-manager/file-manager';
import LabelsEditor from 'components/labels-editor/labels-editor';
import { Files } from 'components/file-manager/file-manager';
import {
getFileContentType,
getContentTypeRemoteFile,
getFileNameFromPath,
} from 'utils/files';
import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form';
import ProjectSearchField from './project-search-field';
import ProjectSubsetField from './project-subset-field';
import MultiTasksProgress from './multi-task-progress';
import AdvancedConfigurationForm, { AdvancedConfiguration, SortingMethod } from './advanced-configuration-form';
type TabName = 'local' | 'share' | 'remote' | 'cloudStorage';
const core = getCore();
export interface CreateTaskData {
@ -33,22 +42,28 @@ export interface CreateTaskData {
advanced: AdvancedConfiguration;
labels: any[];
files: Files;
activeFileManagerTab: string;
activeFileManagerTab: TabName;
cloudStorageId: number | null;
}
interface Props {
onCreate: (data: CreateTaskData) => void;
status: string;
taskId: number | null;
onCreate: (data: CreateTaskData, onProgress?: (status: string, progress?: number) => void) => Promise<any>;
projectId: number | null;
installedGit: boolean;
dumpers:[]
dumpers:[];
many: boolean;
}
type State = CreateTaskData;
type State = CreateTaskData & {
multiTasks: (CreateTaskData & {
status: 'pending' | 'progress' | 'failed' | 'completed' | 'cancelled';
})[];
uploadFileErrorMessage: string;
loading: boolean;
statusInProgressTask: string;
};
const defaultState = {
const defaultState: State = {
projectId: null,
basic: {
name: '',
@ -79,6 +94,15 @@ const defaultState = {
},
activeFileManagerTab: 'local',
cloudStorageId: null,
multiTasks: [],
uploadFileErrorMessage: '',
loading: false,
statusInProgressTask: '',
};
const UploadFileErrorMessages = {
one: 'It can not be processed. You can upload an archive with images, a video or multiple images',
multi: 'It can not be processed. You can upload one or more videos',
};
class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps, State> {
@ -132,23 +156,36 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
};
private validateFiles = (): boolean => {
const { activeFileManagerTab } = this.state;
const files = this.fileManagerContainer.getFiles();
this.setState({
files,
});
const { activeFileManagerTab, files } = this.state;
if (activeFileManagerTab === 'cloudStorage') {
this.setState({
cloudStorageId: this.fileManagerContainer.getCloudStorageId(),
});
}
const totalLen = Object.keys(files).reduce((acc, key) => acc + files[key].length, 0);
const totalLen = Object.keys(files).reduce((acc, key: string) => acc + files[(key as TabName)].length, 0);
return !!totalLen;
};
private startLoading = (): void => {
this.setState({
loading: true,
});
};
private stopLoading = (): void => {
this.setState({
loading: false,
});
};
private changeStatusInProgressTask = (status: string): void => {
this.setState({
statusInProgressTask: status,
});
};
private handleProjectIdChange = (value: null | number): void => {
const { projectId, subset } = this.state;
@ -159,7 +196,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}));
};
private handleSubmitBasicConfiguration = (values: BaseConfiguration): void => {
private handleChangeBasicConfiguration = (values: BaseConfiguration): void => {
this.setState({
basic: { ...values },
});
@ -177,11 +214,9 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
});
};
private changeFileManagerTab = (key: string): void => {
const values = this.state;
private changeFileManagerTab = (value: TabName): void => {
this.setState({
...values,
activeFileManagerTab: key,
activeFileManagerTab: value,
});
};
@ -207,33 +242,125 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
this.basicConfigurationComponent.current?.focus();
};
private handleSubmitAndOpen = (): void => {
const { history } = this.props;
private handleUploadLocalFiles = (uploadedFiles: File[]): void => {
const { many } = this.props;
const { files } = this.state;
this.handleSubmit()
.then((createdTask) => {
const { id } = createdTask;
history.push(`/tasks/${id}`);
})
.catch(() => {});
let uploadFileErrorMessage = '';
if (!many && uploadedFiles.length > 1) {
uploadFileErrorMessage = uploadedFiles.every((it) => (getFileContentType(it) === 'image' || it.name === 'manifest.jsonl')) ? '' : UploadFileErrorMessages.one;
} else if (many) {
uploadFileErrorMessage = uploadedFiles.every((it) => getFileContentType(it) === 'video') ? '' : UploadFileErrorMessages.multi;
}
this.setState({
uploadFileErrorMessage,
});
if (!uploadFileErrorMessage) {
this.setState({
files: {
...files,
local: uploadedFiles,
},
});
}
};
private handleSubmitAndContinue = (): void => {
this.handleSubmit()
.then(() => {
notification.info({
message: 'The task has been created',
className: 'cvat-notification-create-task-success',
});
})
.then(this.resetState)
.then(this.focusToForm)
.catch(() => {});
private handleUploadRemoteFiles = async (urls: string[]): Promise<void> => {
const { many } = this.props;
const { files } = this.state;
const { length } = urls;
let uploadFileErrorMessage = '';
try {
if (!many && length > 1) {
let index = 0;
while (index < length) {
const isImageFile = await getContentTypeRemoteFile(urls[index]) === 'image';
if (!isImageFile) {
uploadFileErrorMessage = UploadFileErrorMessages.one;
break;
}
index++;
}
} else if (many) {
let index = 0;
while (index < length) {
const isVideoFile = await getContentTypeRemoteFile(urls[index]) === 'video';
if (!isVideoFile) {
uploadFileErrorMessage = UploadFileErrorMessages.multi;
break;
}
index++;
}
}
} catch (err) {
uploadFileErrorMessage = `We can't process it. ${err}`;
}
this.setState({
uploadFileErrorMessage,
});
if (!uploadFileErrorMessage) {
this.setState({
files: {
...files,
remote: urls,
},
});
}
};
private handleSubmit = (): Promise<any> => new Promise((resolve, reject) => {
const { projectId } = this.state;
private handleUploadShareFiles = (shareFiles: {
key: string;
type: string;
mime_type: string;
}[]): void => {
const { many } = this.props;
const { files } = this.state;
let uploadFileErrorMessage = '';
if (!many && shareFiles.length > 1) {
uploadFileErrorMessage = shareFiles.every((it) => it.mime_type === 'image') ?
'' : UploadFileErrorMessages.one;
} else if (many) {
uploadFileErrorMessage = shareFiles.every((it) => it.mime_type === 'video') ?
'' : UploadFileErrorMessages.multi;
}
this.setState({
uploadFileErrorMessage,
});
if (!uploadFileErrorMessage) {
this.setState({
files: {
...files,
share: shareFiles.map((it) => it.key),
},
});
}
};
private handleUploadCloudStorageFiles = (cloudStorageFiles: string[]): void => {
const { files } = this.state;
this.setState({
files: {
...files,
cloudStorage: cloudStorageFiles,
},
});
};
private validateBlocks = (): Promise<any> => new Promise((resolve, reject) => {
const { projectId } = this.state;
if (!this.validateLabelsOrProject()) {
notification.error({
message: 'Could not create a task',
@ -287,13 +414,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}
return Promise.resolve();
})
.then((): void => {
const { onCreate } = this.props;
return onCreate(this.state);
})
.then((cratedTask) => {
resolve(cratedTask);
})
.then(resolve)
.catch((error: Error | ValidateErrorEntity): void => {
notification.error({
message: 'Could not create a task',
@ -308,12 +429,252 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
});
});
private handleSubmitAndOpen = (): void => {
const { history } = this.props;
this.validateBlocks()
.then(this.createOneTask)
.then((createdTask) => {
const { id } = createdTask;
history.push(`/tasks/${id}`);
})
.catch(() => {});
};
private handleSubmitAndContinue = (): void => {
this.validateBlocks()
.then(this.createOneTask)
.then(() => {
notification.info({
message: 'The task has been created',
className: 'cvat-notification-create-task-success',
});
})
.then(this.resetState)
.then(this.focusToForm)
.catch(() => {});
};
private createOneTask = (): Promise<any> => {
const { onCreate } = this.props;
this.startLoading();
return onCreate(this.state, this.changeStatusInProgressTask)
.finally(this.stopLoading);
};
private setStatusOneOfMultiTasks = async (index: number, status: string): Promise<void> => {
const { multiTasks } = this.state;
const resultTask = {
...multiTasks[index],
status,
};
return new Promise((resolve) => {
const newMultiTasks: any = [
...multiTasks.slice(0, index),
resultTask,
...multiTasks.slice(index + 1),
];
this.setState({
multiTasks: newMultiTasks,
}, resolve);
});
};
private createOneOfMultiTasks = async (index: any): Promise<void> => {
const { onCreate } = this.props;
const { multiTasks } = this.state;
const task = multiTasks[index];
if (task.status !== 'pending') return;
await this.setStatusOneOfMultiTasks(index, 'progress');
try {
await onCreate(task);
await this.setStatusOneOfMultiTasks(index, 'completed');
} catch (err) {
console.warn(err);
await this.setStatusOneOfMultiTasks(index, 'failed');
}
};
private createMultiTasks = async (): Promise<any> => {
const { multiTasks } = this.state;
this.startLoading();
const { length } = multiTasks;
let index = 0;
const queueSize = 1;
const promises = Array(queueSize)
.fill(undefined)
.map(async (): Promise<void> => {
// eslint-disable-next-line no-constant-condition
while (true) {
index++; // preliminary increase is needed to avoid using the same index when queueSize > 1
if (index > length) break;
await this.createOneOfMultiTasks(index - 1);
}
});
await Promise.allSettled(promises);
this.stopLoading();
};
private addMultiTasks = async (): Promise<void> => new Promise((resolve) => {
const {
projectId,
subset,
advanced,
labels,
files: allFiles,
activeFileManagerTab,
cloudStorageId,
} = this.state;
const files: (File | string)[] = allFiles[activeFileManagerTab];
this.setState({
multiTasks: files.map((file, index) => ({
projectId,
basic: {
name: this.getTaskName(index, activeFileManagerTab),
},
subset,
advanced,
labels,
files: {
...defaultState.files,
[activeFileManagerTab]: [file],
},
activeFileManagerTab,
cloudStorageId,
status: 'pending',
}
)),
}, resolve);
});
private handleSubmitMutliTasks = (): void => {
this.validateBlocks()
.then(() => {
this.addMultiTasks();
})
.then(this.createMultiTasks)
.then(() => {
const { multiTasks } = this.state;
const countCompleted = multiTasks.filter((item) => item.status === 'completed').length;
const countFailed = multiTasks.filter((item) => item.status === 'failed').length;
const countCancelled = multiTasks.filter((item) => item.status === 'cancelled').length;
const countAll = multiTasks.length;
notification.info({
message: 'The tasks have been created',
description:
`Completed: ${countCompleted}, failed: ${countFailed},${countCancelled ?
` cancelled: ${countCancelled},` :
''} total: ${countAll}, `,
className: 'cvat-notification-create-task-success',
});
});
};
private handleCancelMultiTasks = (): void => {
const { multiTasks } = this.state;
let count = 0;
const newMultiTasks: any = multiTasks.map((it) => {
if (it.status === 'pending') {
count++;
return {
...it,
status: 'cancelled',
};
}
return it;
});
this.setState({
multiTasks: newMultiTasks,
}, () => {
notification.info({
message: `Creation of ${count} tasks have been canceled`,
className: 'cvat-notification-create-task-success',
});
});
};
private handleOkMultiTasks = (): void => {
const { history } = this.props;
history.push('/tasks/');
};
private handleRetryCancelledMultiTasks = (): void => {
const { multiTasks } = this.state;
const newMultiTasks: any = multiTasks.map((it) => {
if (it.status === 'cancelled') {
return {
...it,
status: 'pending',
};
}
return it;
});
this.setState({
multiTasks: newMultiTasks,
}, () => {
this.createMultiTasks();
});
};
private handleRetryFailedMultiTasks = (): void => {
const { multiTasks } = this.state;
const newMultiTasks: any = multiTasks.map((it) => {
if (it.status === 'failed') {
return {
...it,
status: 'pending',
};
}
return it;
});
this.setState({
multiTasks: newMultiTasks,
}, () => {
this.createMultiTasks();
});
};
private getTaskName = (indexFile: number, fileManagerTabName: TabName, defaultFileName = ''): string => {
const { many } = this.props;
const { basic } = this.state;
const { files } = this.state;
const file = files[fileManagerTabName][indexFile];
let fileName = defaultFileName;
switch (fileManagerTabName) {
case 'remote':
fileName = getFileNameFromPath(file as string) || defaultFileName;
break;
case 'share':
fileName = getFileNameFromPath(file as string) || defaultFileName;
break;
default:
fileName = (file as File)?.name || (file as string) || defaultFileName;
break;
}
return many ?
basic.name
.replaceAll('{{file_name}}', fileName)
.replaceAll('{{index}}', indexFile.toString()) :
basic.name;
};
private renderBasicBlock(): JSX.Element {
const { many } = this.props;
const exampleMultiTaskName = many ? this.getTaskName(0, 'local', 'fileName.mp4') : '';
return (
<Col span={24}>
<BasicConfigurationForm
ref={this.basicConfigurationComponent}
onSubmit={this.handleSubmitBasicConfiguration}
many={many}
exampleMultiTaskName={exampleMultiTaskName}
onChange={this.handleChangeBasicConfiguration}
/>
</Col>
);
@ -390,17 +751,37 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}
private renderFilesBlock(): JSX.Element {
const { many } = this.props;
const { uploadFileErrorMessage } = this.state;
return (
<Col span={24}>
<Text type='danger'>* </Text>
<Text className='cvat-text-color'>Select files</Text>
<ConnectedFileManager
onChangeActiveKey={this.changeFileManagerTab}
ref={(container: any): void => {
this.fileManagerContainer = container;
}}
/>
</Col>
<>
<Col span={24}>
<Text type='danger'>* </Text>
<Text className='cvat-text-color'>Select files</Text>
<ConnectedFileManager
many={many}
onChangeActiveKey={this.changeFileManagerTab}
onUploadLocalFiles={this.handleUploadLocalFiles}
onUploadRemoteFiles={this.handleUploadRemoteFiles}
onUploadShareFiles={this.handleUploadShareFiles}
onUploadCloudStorageFiles={this.handleUploadCloudStorageFiles}
ref={(container: any): void => {
this.fileManagerContainer = container;
}}
/>
</Col>
{ uploadFileErrorMessage ? (
<Col span={24}>
<Alert
className='cvat-create-task-content-alert'
type='error'
message={uploadFileErrorMessage}
showIcon
/>
</Col>
) : null }
</>
);
}
@ -450,16 +831,21 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
);
}
private renderActions(): JSX.Element {
private renderFooterSingleTask(): JSX.Element {
const { uploadFileErrorMessage, loading, statusInProgressTask: status } = this.state;
if (status === 'FAILED' || loading) {
return (<Alert message={status} />);
}
return (
<Row justify='end' gutter={5}>
<Col>
<Button type='primary' onClick={this.handleSubmitAndOpen}>
<Button type='primary' onClick={this.handleSubmitAndOpen} disabled={!!uploadFileErrorMessage}>
Submit & Open
</Button>
</Col>
<Col>
<Button type='primary' onClick={this.handleSubmitAndContinue}>
<Button type='primary' onClick={this.handleSubmitAndContinue} disabled={!!uploadFileErrorMessage}>
Submit & Continue
</Button>
</Col>
@ -467,9 +853,45 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
);
}
private renderFooterMutliTasks(): JSX.Element {
const {
multiTasks: items,
uploadFileErrorMessage,
files,
activeFileManagerTab,
loading,
} = this.state;
const currentFiles = files[activeFileManagerTab];
const countPending = items.filter((item) => item.status === 'pending').length;
const countAll = items.length;
if ((loading || countPending !== countAll) && currentFiles.length) {
return (
<MultiTasksProgress
tasks={items}
onOk={this.handleOkMultiTasks}
onCancel={this.handleCancelMultiTasks}
onRetryFailedTasks={this.handleRetryFailedMultiTasks}
onRetryCancelledTasks={this.handleRetryCancelledMultiTasks}
/>
);
}
return (
<Row justify='end' gutter={5}>
<Col>
<Button type='primary' onClick={this.handleSubmitMutliTasks} disabled={!!uploadFileErrorMessage}>
Submit&nbsp;
{currentFiles.length}
&nbsp;tasks
</Button>
</Col>
</Row>
);
}
public render(): JSX.Element {
const { status } = this.props;
const loading = !!status && status !== 'CREATED' && status !== 'FAILED';
const { many } = this.props;
return (
<Row justify='start' align='middle' className='cvat-create-task-content'>
@ -485,7 +907,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
{this.renderAdvancedBlock()}
<Col span={24} className='cvat-create-task-content-footer'>
{loading ? <Alert message={status} /> : this.renderActions()}
{many ? this.renderFooterMutliTasks() : this.renderFooterSingleTask() }
</Col>
</Row>
);

@ -1,9 +1,10 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
import Modal from 'antd/lib/modal';
@ -14,26 +15,30 @@ import TextArea from 'antd/lib/input/TextArea';
import CreateTaskContent, { CreateTaskData } from './create-task-content';
interface Props {
onCreate: (data: CreateTaskData) => void;
status: string;
error: string;
taskId: number | null;
onCreate: (data: CreateTaskData, onProgress?: (status: string) => void) => Promise<any>;
installedGit: boolean;
dumpers: []
}
export default function CreateTaskPage(props: Props): JSX.Element {
const {
error, status, taskId, onCreate, installedGit, dumpers,
onCreate, installedGit, dumpers,
} = props;
const location = useLocation();
const [error, setError] = useState('');
let projectId = null;
const params = new URLSearchParams(location.search);
if (params.get('projectId')?.match(/^[1-9]+[0-9]*$/)) {
projectId = +(params.get('projectId') as string);
}
const many = params.get('many') === 'true';
const handleCreate: typeof onCreate = (...onCreateParams) => onCreate(...onCreateParams)
.catch((err) => {
setError(err.toString());
throw err;
});
useEffect(() => {
if (error) {
@ -75,12 +80,11 @@ export default function CreateTaskPage(props: Props): JSX.Element {
<Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Create a new task</Text>
<CreateTaskContent
taskId={taskId}
projectId={projectId}
status={status}
onCreate={onCreate}
onCreate={handleCreate}
installedGit={installedGit}
dumpers={dumpers}
many={many}
/>
</Col>
</Row>

@ -0,0 +1,159 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Alert from 'antd/lib/alert';
import Progress from 'antd/lib/progress';
import Row from 'antd/lib/row';
import Col from 'antd/lib/col';
import Button from 'antd/lib/button';
import Collapse from 'antd/lib/collapse';
import Text from 'antd/lib/typography/Text';
import List from 'antd/lib/list';
interface Props {
tasks: any[];
onCancel: () => void;
onOk: () => void;
onRetryFailedTasks: () => void;
onRetryCancelledTasks: () => void;
}
export default function MultiTasksProgress(props: Props): JSX.Element {
const {
tasks: items,
onOk,
onCancel,
onRetryFailedTasks,
onRetryCancelledTasks,
} = props;
let alertType: any = 'info';
const countPending = items.filter((item) => item.status === 'pending').length;
const countProgress = items.filter((item) => item.status === 'progress').length;
const countCompleted = items.filter((item) => item.status === 'completed').length;
const countFailed = items.filter((item) => item.status === 'failed').length;
const countCancelled = items.filter((item) => item.status === 'cancelled').length;
const countAll = items.length;
const percent = countAll ?
Math.ceil(((countAll - (countPending + countProgress)) / countAll) * 100) :
0;
const failedFiles: string[] = percent === 100 && countFailed ?
items.filter((item) => item.status === 'failed')
.map((item): string => {
const tabs = Object.keys(item.files);
const itemType = tabs.find((key) => (item.files[key][0])) || 'local';
return item.files[itemType][0]?.name || item.files[itemType][0] || '';
})
.filter(Boolean) :
[];
if (percent === 100) {
if (countFailed === countAll) {
alertType = 'error';
} else if (countFailed) {
alertType = 'warning';
}
}
return (
<Alert
type={alertType}
message={(
<div>
{percent === 100 ? (
<Row>
<Col>
Finished
</Col>
</Row>
) : null}
<Row>
<Col>
{`Pending: ${countPending} `}
</Col>
<Col offset={1}>
{`Progress: ${countProgress} `}
</Col>
<Col offset={1}>
{`Completed: ${countCompleted} `}
</Col>
<Col offset={1}>
{`Failed: ${countFailed} `}
</Col>
{countCancelled ? (<Col offset={1}>{`Cancelled: ${countCancelled} `}</Col>) : null}
<Col offset={1}>
{`Total: ${countAll}.`}
</Col>
</Row>
<Progress
status='normal'
percent={percent}
strokeWidth={5}
size='small'
trailColor='#d8d8d8'
/>
<br />
{percent === 100 && countFailed ? (
<Row>
<Collapse style={{
width: '100%',
marginBottom: 5,
}}
>
<Collapse.Panel
header={(
<Text strong>
Failed files
</Text>
)}
key='appearance'
>
<List
size='small'
dataSource={failedFiles}
renderItem={(item: string) => <List.Item>{ item }</List.Item>}
/>
</Collapse.Panel>
</Collapse>
</Row>
) : null }
<Row justify='end' gutter={5}>
{percent === 100 ?
(
<>
<Col>
<Button disabled={!countFailed} onClick={onRetryFailedTasks}>
Retry failed tasks
</Button>
</Col>
{
countCancelled ? (
<Col>
<Button disabled={!countCancelled} onClick={onRetryCancelledTasks}>
Retry cancelled tasks
</Button>
</Col>
) : null
}
<Col>
<Button type='primary' onClick={onOk}>
Ok
</Button>
</Col>
</>
) : (
<Col>
<Button onClick={onCancel} disabled={!countPending}>
Cancel pending tasks
</Button>
</Col>
)}
</Row>
</div>
)}
/>
);
}

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -9,17 +10,17 @@ import Tabs from 'antd/lib/tabs';
import Input from 'antd/lib/input';
import Text from 'antd/lib/typography/Text';
import Paragraph from 'antd/lib/typography/Paragraph';
import Upload, { RcFile } from 'antd/lib/upload';
import { RcFile } from 'antd/lib/upload';
import Empty from 'antd/lib/empty';
import Tree, { TreeNodeNormal } from 'antd/lib/tree/Tree';
import { FormInstance } from 'antd/lib/form';
// eslint-disable-next-line import/no-extraneous-dependencies
import { EventDataNode } from 'rc-tree/lib/interface';
import { InboxOutlined } from '@ant-design/icons';
import consts from 'consts';
import { CloudStorage } from 'reducers';
import CloudStorageTab from './cloud-storages-tab';
import LocalFiles from './local-files';
export interface Files {
local: File[];
@ -37,9 +38,15 @@ interface State {
}
interface Props {
treeData: TreeNodeNormal[];
onLoadData: (key: string, success: () => void, failure: () => void) => void;
treeData: (TreeNodeNormal & { mime_type: string })[];
share: any;
many: boolean;
onLoadData: (key: string) => Promise<any>;
onChangeActiveKey(key: string): void;
onUploadLocalFiles(files: File[]): void;
onUploadRemoteFiles(urls: string[]): void;
onUploadShareFiles(keys: string[]): Promise<void>;
onUploadCloudStorageFiles(cloudStorageFiles: string[]): void;
}
export class FileManager extends React.PureComponent<Props, State> {
@ -48,6 +55,7 @@ export class FileManager extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.cloudStorageTabFormRef = React.createRef<FormInstance>();
const { onLoadData } = this.props;
this.state = {
files: {
@ -62,17 +70,19 @@ export class FileManager extends React.PureComponent<Props, State> {
active: 'local',
};
this.loadData('/');
onLoadData('/');
}
private onSelectCloudStorageFiles = (cloudStorageFiles: string[]): void => {
private handleUploadCloudStorageFiles = (cloudStorageFiles: string[]): void => {
const { files } = this.state;
const { onUploadCloudStorageFiles } = this.props;
this.setState({
files: {
...files,
cloudStorage: cloudStorageFiles,
},
});
onUploadCloudStorageFiles(cloudStorageFiles);
};
public getCloudStorageId(): number | null {
@ -90,14 +100,6 @@ export class FileManager extends React.PureComponent<Props, State> {
};
}
private loadData = (key: string): Promise<void> => new Promise<void>((resolve, reject): void => {
const { onLoadData } = this.props;
const success = (): void => resolve();
const failure = (): void => reject();
onLoadData(key, success, failure);
});
public reset(): void {
const { active } = this.state;
if (active === 'cloudStorage') {
@ -118,65 +120,43 @@ export class FileManager extends React.PureComponent<Props, State> {
}
private renderLocalSelector(): JSX.Element {
const { many, onUploadLocalFiles } = this.props;
const { files } = this.state;
return (
<Tabs.TabPane className='cvat-file-manager-local-tab' key='local' tab='My computer'>
<Upload.Dragger
multiple
listType='text'
fileList={files.local as any[]}
showUploadList={
files.local.length < 5 && {
showRemoveIcon: false,
}
}
beforeUpload={(_: RcFile, newLocalFiles: RcFile[]): boolean => {
<LocalFiles
files={files.local}
many={many}
onUpload={(_: RcFile, newLocalFiles: RcFile[]): boolean => {
this.setState({
files: {
...files,
local: newLocalFiles,
},
});
onUploadLocalFiles(newLocalFiles);
return false;
}}
>
<p className='ant-upload-drag-icon'>
<InboxOutlined />
</p>
<p className='ant-upload-text'>Click or drag files to this area</p>
<p className='ant-upload-hint'>Support for a bulk images or a single video</p>
</Upload.Dragger>
{files.local.length >= 5 && (
<>
<br />
<Text className='cvat-text-color'>{`${files.local.length} files selected`}</Text>
</>
)}
/>
</Tabs.TabPane>
);
}
private renderShareSelector(): JSX.Element {
function renderTreeNodes(data: TreeNodeNormal[]): JSX.Element[] {
function getTreeNodes(data: TreeNodeNormal[]): TreeNodeNormal[] {
// sort alphabetically
data.sort((a: TreeNodeNormal, b: TreeNodeNormal): number => (
a.key.toLocaleString().localeCompare(b.key.toLocaleString())));
return data.map((item: TreeNodeNormal) => {
if (item.children) {
return (
<Tree.TreeNode title={item.title} key={item.key} data={item} isLeaf={item.isLeaf}>
{renderTreeNodes(item.children)}
</Tree.TreeNode>
);
}
return <Tree.TreeNode {...item} key={item.key} data={item} />;
});
return data
.sort((a: TreeNodeNormal, b: TreeNodeNormal): number => (
a.key.toLocaleString().localeCompare(b.key.toLocaleString())))
.map((it) => ({
...it,
children: it.children ? getTreeNodes(it.children) : undefined,
}));
}
const { SHARE_MOUNT_GUIDE_URL } = consts;
const { treeData } = this.props;
const { treeData, onUploadShareFiles, onLoadData } = this.props;
const { expandedKeys, files } = this.state;
return (
@ -190,7 +170,7 @@ export class FileManager extends React.PureComponent<Props, State> {
checkStrictly={false}
expandedKeys={expandedKeys}
checkedKeys={files.share}
loadData={(event: EventDataNode): Promise<void> => this.loadData(event.key.toLocaleString())}
loadData={(event: EventDataNode): Promise<void> => onLoadData(event.key.toLocaleString())}
onExpand={(newExpandedKeys: ReactText[]): void => {
this.setState({
expandedKeys: newExpandedKeys.map((text: ReactText): string => text.toLocaleString()),
@ -212,10 +192,10 @@ export class FileManager extends React.PureComponent<Props, State> {
share: keys,
},
});
onUploadShareFiles(keys).then().catch();
}}
>
{renderTreeNodes(treeData)}
</Tree>
treeData={getTreeNodes(treeData)}
/>
) : (
<div className='cvat-empty-share-tree'>
<Empty />
@ -233,6 +213,7 @@ export class FileManager extends React.PureComponent<Props, State> {
}
private renderRemoteSelector(): JSX.Element {
const { onUploadRemoteFiles } = this.props;
const { files } = this.state;
return (
@ -243,12 +224,14 @@ export class FileManager extends React.PureComponent<Props, State> {
rows={6}
value={[...files.remote].join('\n')}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
const urls = event.target.value.split('\n');
this.setState({
files: {
...files,
remote: event.target.value.split('\n'),
remote: urls,
},
});
onUploadRemoteFiles(urls.filter(Boolean));
}}
/>
</Tabs.TabPane>
@ -274,14 +257,14 @@ export class FileManager extends React.PureComponent<Props, State> {
setSearchPhrase={(_potentialCloudStorage: string) => {
this.setState({ potentialCloudStorage: _potentialCloudStorage });
}}
onSelectFiles={this.onSelectCloudStorageFiles}
onSelectFiles={this.handleUploadCloudStorageFiles}
/>
</Tabs.TabPane>
);
}
public render(): JSX.Element {
const { onChangeActiveKey } = this.props;
const { onChangeActiveKey, many } = this.props;
const { active } = this.state;
return (
@ -300,7 +283,7 @@ export class FileManager extends React.PureComponent<Props, State> {
{this.renderLocalSelector()}
{this.renderShareSelector()}
{this.renderRemoteSelector()}
{this.renderCloudStorageSelector()}
{!many && this.renderCloudStorageSelector()}
</Tabs>
</>
);

@ -0,0 +1,50 @@
// Copyright (C) 2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Text from 'antd/lib/typography/Text';
import Upload, { RcFile } from 'antd/lib/upload';
import { InboxOutlined } from '@ant-design/icons';
interface Props {
files: File[];
many: boolean;
onUpload: (_: RcFile, uploadedFiles: RcFile[]) => boolean;
}
export default function LocalFiles(props: Props): JSX.Element {
const { files, onUpload, many } = props;
const hintText = many ? 'You can upload one or more videos' :
'You can upload an archive with images, a video, or multiple images';
return (
<>
<Upload.Dragger
multiple
listType='text'
fileList={files as any[]}
showUploadList={
files.length < 5 && {
showRemoveIcon: false,
}
}
beforeUpload={onUpload}
>
<p className='ant-upload-drag-icon'>
<InboxOutlined />
</p>
<p className='ant-upload-text'>Click or drag files to this area</p>
<p className='ant-upload-hint'>{ hintText }</p>
</Upload.Dragger>
{files.length >= 5 && (
<>
<br />
<Text className='cvat-text-color'>{`${files.length} files selected`}</Text>
</>
)}
</>
);
}

@ -19,6 +19,7 @@ import { usePrevious } from 'utils/hooks';
import {
localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config,
} from './tasks-filter-configuration';
import { MutliPlusIcon } from '../../icons';
const FilteringComponent = ResourceFilterHOC(
config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues,
@ -101,6 +102,14 @@ export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element
>
Create a new task
</Button>
<Button
className='cvat-create-multi-tasks-button'
type='primary'
onClick={(): void => history.push('/tasks/create?many=true')}
icon={<span className='anticon'><MutliPlusIcon /></span>}
>
Create multi tasks
</Button>
<Button
className='cvat-import-task-button'
type='primary'

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -10,27 +11,22 @@ import { CreateTaskData } from 'components/create-task-page/create-task-content'
import { createTaskAsync } from 'actions/tasks-actions';
interface StateToProps {
taskId: number | null;
status: string;
error: string;
installedGit: boolean;
dumpers:[]
}
interface DispatchToProps {
onCreate: (data: CreateTaskData) => void;
onCreate: (data: CreateTaskData, onProgress?: (status: string) => void) => Promise<any>;
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onCreate: (data: CreateTaskData): Promise<any> => dispatch(createTaskAsync(data)),
onCreate: (data, onProgress) => dispatch(createTaskAsync(data, onProgress)),
};
}
function mapStateToProps(state: CombinedState): StateToProps {
const { creates } = state.tasks.activities;
return {
...creates,
installedGit: state.plugins.list.GIT_INTEGRATION,
dumpers: state.formats.annotationFormats.dumpers,
};

@ -1,39 +1,53 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import { uniqBy } from 'lodash';
import { TreeNodeNormal } from 'antd/lib/tree/Tree';
import FileManagerComponent, { Files } from 'components/file-manager/file-manager';
import { loadShareDataAsync } from 'actions/share-actions';
import { ShareItem, CombinedState } from 'reducers';
import { ShareItem, CombinedState, ShareFileInfo } from 'reducers';
interface OwnProps {
ref: any;
many: boolean
onChangeActiveKey(key: string): void;
onUploadLocalFiles(files: File[]): void;
onUploadRemoteFiles(urls: string[]): void;
onUploadShareFiles(shareFiles: {
key: string;
type: string;
mime_type: string;
}[]): void;
onUploadCloudStorageFiles(urls: string[]): void;
}
interface StateToProps {
treeData: TreeNodeNormal[];
treeData: (TreeNodeNormal & { mime_type: string })[];
share: any;
}
interface DispatchToProps {
getTreeData(key: string, success: () => void, failure: () => void): void;
getTreeData(key: string): Promise<ShareFileInfo[]>;
}
function mapStateToProps(state: CombinedState): StateToProps {
function convert(items: ShareItem[], path?: string): TreeNodeNormal[] {
function convert(items: ShareItem[], path?: string): (TreeNodeNormal & { mime_type: string })[] {
return items.map(
(item): TreeNodeNormal => {
(item): (TreeNodeNormal & { mime_type: string }) => {
const isLeaf = item.type !== 'DIR';
const key = `${path}${item.name}${isLeaf ? '' : '/'}`;
return {
key,
isLeaf,
title: item.name || 'root',
mime_type: item.mime_type,
children: convert(item.children, key),
};
},
@ -41,16 +55,17 @@ function mapStateToProps(state: CombinedState): StateToProps {
}
const { root } = state.share;
return {
treeData: convert([root], ''),
share: state.share,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
getTreeData: (key: string, success: () => void, failure: () => void): void => {
dispatch(loadShareDataAsync(key, success, failure));
},
getTreeData: (key: string):
Promise<ShareFileInfo[]> => dispatch(loadShareDataAsync(key)),
};
}
@ -65,6 +80,63 @@ export class FileManagerContainer extends React.PureComponent<Props> {
this.managerComponentRef = React.createRef();
}
private handleUploadShareFiles = (keys: string[]): Promise<void> => new Promise(() => {
const { onUploadShareFiles, getTreeData } = this.props;
const getItemTreeDataByPath = (data: any, partsPath: string[]): any => {
if (partsPath.length === 1) return data.children.find((child: any) => child.title === partsPath[0]);
return getItemTreeDataByPath(
data.children.find((child: any) => child.title === partsPath[0]),
[...partsPath].filter((it, index) => index !== 0),
);
};
const getShareFiles = async (localKeys: string[]): Promise<{
key: string;
type: string;
mime_type: string;
}[]> => {
const { treeData } = this.props;
let files: {
key: string;
type: string;
mime_type: string;
}[] = [];
for await (const key of localKeys) {
const partsPath = key.split('/').filter(Boolean);
const itemTreeData = partsPath.length ? getItemTreeDataByPath(treeData[0], partsPath) : treeData[0];
console.log(itemTreeData);
if (itemTreeData.isLeaf) {
files = [...files, {
...itemTreeData,
key,
type: itemTreeData.type,
mime_type: itemTreeData.mime_type,
}];
} else {
const children: {
key: string;
type: string;
mime_type: string;
}[] = await getTreeData(key)
.then((items) => items.map(
(item): string => {
const isLeaf = item.type !== 'DIR';
return `${key}${item.name}${isLeaf ? '' : '/'}`;
},
))
.then(getShareFiles);
files = [...files, ...children];
}
}
return files;
};
getShareFiles(keys).then((data) => {
onUploadShareFiles(uniqBy(data, 'key'));
});
});
public getFiles(): Files {
return this.managerComponentRef.getFiles();
}
@ -78,12 +150,27 @@ export class FileManagerContainer extends React.PureComponent<Props> {
}
public render(): JSX.Element {
const { treeData, getTreeData, onChangeActiveKey } = this.props;
const {
treeData,
share,
getTreeData,
many,
onChangeActiveKey,
onUploadLocalFiles,
onUploadRemoteFiles,
onUploadCloudStorageFiles,
} = this.props;
return (
<FileManagerComponent
treeData={treeData}
share={share}
many={many}
onLoadData={getTreeData}
onUploadLocalFiles={onUploadLocalFiles}
onUploadRemoteFiles={onUploadRemoteFiles}
onUploadShareFiles={this.handleUploadShareFiles}
onUploadCloudStorageFiles={onUploadCloudStorageFiles}
onChangeActiveKey={onChangeActiveKey}
ref={(component): void => {
this.managerComponentRef = component;

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -52,6 +53,7 @@ import SVGCVATAzureProvider from './assets/vscode-icons_file-type-azure.svg';
import SVGCVATS3Provider from './assets/S3.svg';
import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg';
import SVGRestoreIcon from './assets/restore-icon.svg';
import SVGMultiPlusIcon from './assets/multi-plus-icon.svg';
export const CVATLogo = React.memo((): JSX.Element => <SVGCVATLogo />);
export const CursorIcon = React.memo((): JSX.Element => <SVGCursorIcon />);
@ -101,3 +103,4 @@ export const AzureProvider = React.memo((): JSX.Element => <SVGCVATAzureProvider
export const S3Provider = React.memo((): JSX.Element => <SVGCVATS3Provider />);
export const GoogleCloudProvider = React.memo((): JSX.Element => <SVGCVATGoogleCloudProvider />);
export const RestoreIcon = React.memo((): JSX.Element => <SVGRestoreIcon />);
export const MutliPlusIcon = React.memo((): JSX.Element => <SVGMultiPlusIcon />);

@ -3,6 +3,7 @@
//
// SPDX-License-Identifier: MIT
// eslint-disable-next-line import/no-extraneous-dependencies
import { Canvas3d } from 'cvat-canvas3d/src/typescript/canvas3d';
import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper';
import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
@ -100,11 +101,6 @@ export interface TasksState {
deletes: {
[tid: number]: boolean; // deleted (deleting if in dictionary)
};
creates: {
taskId: number | null;
status: string;
error: string;
};
jobUpdates: {
[jid: number]: boolean,
};
@ -296,11 +292,13 @@ export interface ShareFileInfo {
// get this data from cvat-core
name: string;
type: 'DIR' | 'REG';
mime_type: string;
}
export interface ShareItem {
name: string;
type: 'DIR' | 'REG';
mime_type: string;
children: ShareItem[];
}

@ -33,11 +33,6 @@ const defaultState: TasksState = {
},
activities: {
deletes: {},
creates: {
taskId: null,
status: '',
error: '',
},
jobUpdates: {},
},
};
@ -128,60 +123,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
},
};
}
case TasksActionTypes.CREATE_TASK: {
return {
...state,
activities: {
...state.activities,
creates: {
taskId: null,
status: '',
error: '',
},
},
};
}
case TasksActionTypes.CREATE_TASK_STATUS_UPDATED: {
const { status } = action.payload;
return {
...state,
activities: {
...state.activities,
creates: {
...state.activities.creates,
status,
},
},
};
}
case TasksActionTypes.CREATE_TASK_SUCCESS: {
const { taskId } = action.payload;
return {
...state,
activities: {
...state.activities,
creates: {
...state.activities.creates,
taskId,
status: 'CREATED',
},
},
};
}
case TasksActionTypes.CREATE_TASK_FAILED: {
return {
...state,
activities: {
...state.activities,
creates: {
...state.activities.creates,
status: 'FAILED',
error: action.payload.error.toString(),
},
},
};
}
case TasksActionTypes.UPDATE_TASK: {
return {
...state,

@ -0,0 +1,106 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
export function getFileContentTypeByMimeType(mime_type: string): string {
return mime_type.split('/')[0];
}
export function getFileContentType(file: File): string {
return getFileContentTypeByMimeType(file.type);
}
export function checkFileTypesEqual(files: File[]): boolean {
if (!files.length) return true;
const typeFirstFile: string = getFileContentType(files[0]);
return files.every((file) => getFileContentType(file) === typeFirstFile);
}
function checkCreatingVideoElement(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const el = document.createElement('video');
el.src = url;
el.onloadedmetadata = () => {
el.remove();
resolve();
};
el.onerror = reject;
});
}
function checkCreatingImageElement(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const el = document.createElement('img');
el.src = url;
el.onload = () => {
el.remove();
resolve();
};
el.onerror = reject;
});
}
function getUrlExtension(url: string): string {
return url.split(/[#?]/)[0].split('.').pop()?.trim() || '';
}
// source https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
const IMAGE_EXTENSIONS = ['apng', 'avif', 'gif', 'jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'png', 'webp'];
// source https://en.wikipedia.org/wiki/Video_file_format
const VIDEO_EXTENSIONS = ['webm', 'mkv', 'flv', 'flv', 'vob', 'ogv', 'ogg', 'drc', 'gifv', 'mng', 'avi', 'MTS',
'M2TS', 'TS', 'mov', 'qt', 'wmv', 'yuv', 'rm', 'rmvb', 'viv', 'asf', 'amv', 'mp4', 'm4p', 'm4v',
'mpg', 'mp2', 'mpeg', 'mpe', 'mpv', 'mpg', 'mpeg', 'm2v', 'm4v', 'svi', '3gp', '3g2', 'mxf', 'roq',
'nsv', 'flv', 'f4v', 'f4p', 'f4a', 'f4b',
];
export function getContentTypeRemoteFile(url: string): Promise<string> {
return new Promise((resolve): void => {
const extention = getUrlExtension(url);
if (IMAGE_EXTENSIONS.includes(extention)) {
resolve('image');
return;
}
if (VIDEO_EXTENSIONS.includes(extention)) {
resolve('video');
return;
}
const xhr = new XMLHttpRequest();
xhr.open('HEAD', url, true);
xhr.onreadystatechange = () => {
const contentType = xhr.getResponseHeader('Content-Type');
if (xhr.status > 299) {
xhr.abort();
resolve('unknown');
return;
}
if (contentType) {
xhr.abort();
resolve(contentType.split('/')[0]);
}
};
xhr.onerror = () => {
checkCreatingVideoElement(url)
.then(() => {
resolve('video');
})
.catch(() => {
checkCreatingImageElement(url);
})
.then(() => {
resolve('image');
})
.catch(() => {
resolve('unknown');
});
};
xhr.send();
});
}
export function getFileNameFromPath(path: string): string {
return path.split('/').filter(Boolean).pop()?.split(/[#?]/)?.[0] || '';
}

@ -960,6 +960,7 @@ class LabeledDataSerializer(serializers.Serializer):
class FileInfoSerializer(serializers.Serializer):
name = serializers.CharField(max_length=1024)
type = serializers.ChoiceField(choices=["REG", "DIR"])
mime_type = serializers.CharField(max_length=255)
class LogEventSerializer(serializers.Serializer):
job_id = serializers.IntegerField(required=False)

@ -52,6 +52,7 @@ from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer
from cvat.apps.engine.frame_provider import FrameProvider
from cvat.apps.engine.media_extractors import ImageListReader
from cvat.apps.engine.mime_types import mimetypes
from cvat.apps.engine.media_extractors import get_mime
from cvat.apps.engine.models import (
Job, Task, Project, Issue, Data,
Comment, StorageMethodChoice, StorageChoice, Image,
@ -186,13 +187,20 @@ class ServerViewSet(viewsets.ViewSet):
content = os.scandir(directory)
for entry in content:
entry_type = None
entry_mime_type = None
if entry.is_file():
entry_type = "REG"
entry_mime_type = get_mime(os.path.join(settings.SHARE_ROOT, entry))
elif entry.is_dir():
entry_type = "DIR"
entry_mime_type = "DIR"
if entry_type:
data.append({"name": entry.name, "type": entry_type})
data.append({
"name": entry.name,
"type": entry_type,
"mime_type": entry_mime_type,
})
serializer = FileInfoSerializer(many=True, data=data)
if serializer.is_valid(raise_exception=True):

@ -799,9 +799,10 @@ Cypress.Commands.add('interactMenu', (choice) => {
Cypress.Commands.add('setJobState', (choice) => {
cy.interactMenu('Change job state');
cy.get('.cvat-annotation-menu-job-state-submenu').within(() => {
cy.contains(choice).click();
});
cy.get('.cvat-annotation-menu-job-state-submenu')
.should('not.have.class', 'ant-zoom-big').within(() => {
cy.contains(choice).click();
});
cy.get('.cvat-modal-content-change-job-state')
.should('be.visible')
.within(() => {

Loading…
Cancel
Save