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

@ -20,9 +20,6 @@ export enum TasksActionTypes {
DELETE_TASK = 'DELETE_TASK', DELETE_TASK = 'DELETE_TASK',
DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS', DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS',
DELETE_TASK_FAILED = 'DELETE_TASK_FAILED', 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', CREATE_TASK_FAILED = 'CREATE_TASK_FAILED',
UPDATE_TASK = 'UPDATE_TASK', UPDATE_TASK = 'UPDATE_TASK',
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS', 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 { function createTaskFailed(error: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.CREATE_TASK_FAILED, type: TasksActionTypes.CREATE_TASK_FAILED,
@ -175,19 +152,9 @@ function createTaskFailed(error: any): AnyAction {
return action; return action;
} }
function createTaskUpdateStatus(status: string): AnyAction { export function createTaskAsync(data: any, onProgress?: (status: string) => void):
const action = { ThunkAction<Promise<void>, {}, {}, AnyAction> {
type: TasksActionTypes.CREATE_TASK_STATUS_UPDATED, return async (dispatch): Promise<any> => {
payload: {
status,
},
};
return action;
}
export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<any> => {
const description: any = { const description: any = {
name: data.basic.name, name: data.basic.name,
labels: data.labels, labels: data.labels,
@ -246,7 +213,7 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
if (gitPlugin) { if (gitPlugin) {
gitPlugin.callbacks.onStatusChange = (status: string): void => { gitPlugin.callbacks.onStatusChange = (status: string): void => {
dispatch(createTaskUpdateStatus(status)); onProgress?.(status);
}; };
gitPlugin.data.task = taskInstance; gitPlugin.data.task = taskInstance;
gitPlugin.data.repos = data.advanced.repository; gitPlugin.data.repos = data.advanced.repository;
@ -255,12 +222,10 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
} }
} }
dispatch(createTask());
try { try {
const savedTask = await taskInstance.save((status: string, progress: number): void => { 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; return savedTask;
} catch (error) { } catch (error) {
dispatch(createTaskFailed(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) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React, { RefObject } from 'react'; import React, { RefObject } from 'react';
import Input from 'antd/lib/input'; 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 Form, { FormInstance } from 'antd/lib/form';
import { Store } from 'antd/lib/form/interface'; import { QuestionCircleOutlined } from '@ant-design/icons';
export interface BaseConfiguration { export interface BaseConfiguration {
name: string; name: string;
} }
interface Props { interface Props {
onSubmit(values: BaseConfiguration): void; onChange(values: BaseConfiguration): void;
many: boolean;
exampleMultiTaskName?: string;
} }
export default class BasicConfigurationForm extends React.PureComponent<Props> { export default class BasicConfigurationForm extends React.PureComponent<Props> {
private formRef: RefObject<FormInstance>; private formRef: RefObject<FormInstance>;
private inputRef: RefObject<Input>; private inputRef: RefObject<Input>;
private initialName: string;
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
this.formRef = React.createRef<FormInstance>(); this.formRef = React.createRef<FormInstance>();
this.inputRef = React.createRef<Input>(); 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> { public submit(): Promise<void> {
const { onSubmit } = this.props;
if (this.formRef.current) { if (this.formRef.current) {
return this.formRef.current.validateFields().then((values: Store): Promise<void> => { return this.formRef.current.validateFields();
onSubmit({ name: values.name });
return Promise.resolve();
});
} }
return Promise.reject(new Error('Form ref is empty')); 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 { public render(): JSX.Element {
const { many, exampleMultiTaskName } = this.props;
return ( return (
<Form ref={this.formRef} layout='vertical'> <Form ref={this.formRef} layout='vertical'>
<Form.Item <Form.Item
@ -62,9 +83,48 @@ export default class BasicConfigurationForm extends React.PureComponent<Props> {
message: 'Task name cannot be empty', message: 'Task name cannot be empty',
}, },
]} ]}
initialValue={this.initialName}
> >
<Input ref={this.inputRef} /> <Input
ref={this.inputRef}
onChange={(e) => this.handleChangeName(e)}
/>
</Form.Item> </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> </Form>
); );
} }

@ -7,11 +7,11 @@ import React, { RefObject } from 'react';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Alert from 'antd/lib/alert';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Collapse from 'antd/lib/collapse'; import Collapse from 'antd/lib/collapse';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import Alert from 'antd/lib/alert';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { ValidateErrorEntity } from 'rc-field-form/lib/interface'; import { ValidateErrorEntity } from 'rc-field-form/lib/interface';
import { StorageLocation } from 'reducers'; import { StorageLocation } from 'reducers';
@ -19,11 +19,20 @@ import { getCore, Storage } from 'cvat-core-wrapper';
import ConnectedFileManager from 'containers/file-manager/file-manager'; import ConnectedFileManager from 'containers/file-manager/file-manager';
import LabelsEditor from 'components/labels-editor/labels-editor'; import LabelsEditor from 'components/labels-editor/labels-editor';
import { Files } from 'components/file-manager/file-manager'; import { Files } from 'components/file-manager/file-manager';
import {
getFileContentType,
getContentTypeRemoteFile,
getFileNameFromPath,
} from 'utils/files';
import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form'; import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form';
import ProjectSearchField from './project-search-field'; import ProjectSearchField from './project-search-field';
import ProjectSubsetField from './project-subset-field'; import ProjectSubsetField from './project-subset-field';
import MultiTasksProgress from './multi-task-progress';
import AdvancedConfigurationForm, { AdvancedConfiguration, SortingMethod } from './advanced-configuration-form'; import AdvancedConfigurationForm, { AdvancedConfiguration, SortingMethod } from './advanced-configuration-form';
type TabName = 'local' | 'share' | 'remote' | 'cloudStorage';
const core = getCore(); const core = getCore();
export interface CreateTaskData { export interface CreateTaskData {
@ -33,22 +42,28 @@ export interface CreateTaskData {
advanced: AdvancedConfiguration; advanced: AdvancedConfiguration;
labels: any[]; labels: any[];
files: Files; files: Files;
activeFileManagerTab: string; activeFileManagerTab: TabName;
cloudStorageId: number | null; cloudStorageId: number | null;
} }
interface Props { interface Props {
onCreate: (data: CreateTaskData) => void; onCreate: (data: CreateTaskData, onProgress?: (status: string, progress?: number) => void) => Promise<any>;
status: string;
taskId: number | null;
projectId: number | null; projectId: number | null;
installedGit: boolean; 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, projectId: null,
basic: { basic: {
name: '', name: '',
@ -79,6 +94,15 @@ const defaultState = {
}, },
activeFileManagerTab: 'local', activeFileManagerTab: 'local',
cloudStorageId: null, 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> { class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps, State> {
@ -132,23 +156,36 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}; };
private validateFiles = (): boolean => { private validateFiles = (): boolean => {
const { activeFileManagerTab } = this.state; const { activeFileManagerTab, files } = this.state;
const files = this.fileManagerContainer.getFiles();
this.setState({
files,
});
if (activeFileManagerTab === 'cloudStorage') { if (activeFileManagerTab === 'cloudStorage') {
this.setState({ this.setState({
cloudStorageId: this.fileManagerContainer.getCloudStorageId(), 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; 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 => { private handleProjectIdChange = (value: null | number): void => {
const { projectId, subset } = this.state; 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({ this.setState({
basic: { ...values }, basic: { ...values },
}); });
@ -177,11 +214,9 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}); });
}; };
private changeFileManagerTab = (key: string): void => { private changeFileManagerTab = (value: TabName): void => {
const values = this.state;
this.setState({ this.setState({
...values, activeFileManagerTab: value,
activeFileManagerTab: key,
}); });
}; };
@ -207,33 +242,125 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
this.basicConfigurationComponent.current?.focus(); this.basicConfigurationComponent.current?.focus();
}; };
private handleSubmitAndOpen = (): void => { private handleUploadLocalFiles = (uploadedFiles: File[]): void => {
const { history } = this.props; const { many } = this.props;
const { files } = this.state;
this.handleSubmit() let uploadFileErrorMessage = '';
.then((createdTask) => {
const { id } = createdTask; if (!many && uploadedFiles.length > 1) {
history.push(`/tasks/${id}`); uploadFileErrorMessage = uploadedFiles.every((it) => (getFileContentType(it) === 'image' || it.name === 'manifest.jsonl')) ? '' : UploadFileErrorMessages.one;
}) } else if (many) {
.catch(() => {}); uploadFileErrorMessage = uploadedFiles.every((it) => getFileContentType(it) === 'video') ? '' : UploadFileErrorMessages.multi;
}
this.setState({
uploadFileErrorMessage,
});
if (!uploadFileErrorMessage) {
this.setState({
files: {
...files,
local: uploadedFiles,
},
});
}
}; };
private handleSubmitAndContinue = (): void => { private handleUploadRemoteFiles = async (urls: string[]): Promise<void> => {
this.handleSubmit() const { many } = this.props;
.then(() => {
notification.info({ const { files } = this.state;
message: 'The task has been created', const { length } = urls;
className: 'cvat-notification-create-task-success',
}); let uploadFileErrorMessage = '';
})
.then(this.resetState) try {
.then(this.focusToForm) if (!many && length > 1) {
.catch(() => {}); 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) => { private handleUploadShareFiles = (shareFiles: {
const { projectId } = this.state; 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()) { if (!this.validateLabelsOrProject()) {
notification.error({ notification.error({
message: 'Could not create a task', message: 'Could not create a task',
@ -287,13 +414,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
} }
return Promise.resolve(); return Promise.resolve();
}) })
.then((): void => { .then(resolve)
const { onCreate } = this.props;
return onCreate(this.state);
})
.then((cratedTask) => {
resolve(cratedTask);
})
.catch((error: Error | ValidateErrorEntity): void => { .catch((error: Error | ValidateErrorEntity): void => {
notification.error({ notification.error({
message: 'Could not create a task', 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 { private renderBasicBlock(): JSX.Element {
const { many } = this.props;
const exampleMultiTaskName = many ? this.getTaskName(0, 'local', 'fileName.mp4') : '';
return ( return (
<Col span={24}> <Col span={24}>
<BasicConfigurationForm <BasicConfigurationForm
ref={this.basicConfigurationComponent} ref={this.basicConfigurationComponent}
onSubmit={this.handleSubmitBasicConfiguration} many={many}
exampleMultiTaskName={exampleMultiTaskName}
onChange={this.handleChangeBasicConfiguration}
/> />
</Col> </Col>
); );
@ -390,17 +751,37 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
} }
private renderFilesBlock(): JSX.Element { private renderFilesBlock(): JSX.Element {
const { many } = this.props;
const { uploadFileErrorMessage } = this.state;
return ( return (
<Col span={24}> <>
<Text type='danger'>* </Text> <Col span={24}>
<Text className='cvat-text-color'>Select files</Text> <Text type='danger'>* </Text>
<ConnectedFileManager <Text className='cvat-text-color'>Select files</Text>
onChangeActiveKey={this.changeFileManagerTab} <ConnectedFileManager
ref={(container: any): void => { many={many}
this.fileManagerContainer = container; onChangeActiveKey={this.changeFileManagerTab}
}} onUploadLocalFiles={this.handleUploadLocalFiles}
/> onUploadRemoteFiles={this.handleUploadRemoteFiles}
</Col> 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 ( return (
<Row justify='end' gutter={5}> <Row justify='end' gutter={5}>
<Col> <Col>
<Button type='primary' onClick={this.handleSubmitAndOpen}> <Button type='primary' onClick={this.handleSubmitAndOpen} disabled={!!uploadFileErrorMessage}>
Submit & Open Submit & Open
</Button> </Button>
</Col> </Col>
<Col> <Col>
<Button type='primary' onClick={this.handleSubmitAndContinue}> <Button type='primary' onClick={this.handleSubmitAndContinue} disabled={!!uploadFileErrorMessage}>
Submit & Continue Submit & Continue
</Button> </Button>
</Col> </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 { public render(): JSX.Element {
const { status } = this.props; const { many } = this.props;
const loading = !!status && status !== 'CREATED' && status !== 'FAILED';
return ( return (
<Row justify='start' align='middle' className='cvat-create-task-content'> <Row justify='start' align='middle' className='cvat-create-task-content'>
@ -485,7 +907,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
{this.renderAdvancedBlock()} {this.renderAdvancedBlock()}
<Col span={24} className='cvat-create-task-content-footer'> <Col span={24} className='cvat-create-task-content-footer'>
{loading ? <Alert message={status} /> : this.renderActions()} {many ? this.renderFooterMutliTasks() : this.renderFooterSingleTask() }
</Col> </Col>
</Row> </Row>
); );

@ -1,9 +1,10 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router'; import { useLocation } from 'react-router';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
@ -14,26 +15,30 @@ import TextArea from 'antd/lib/input/TextArea';
import CreateTaskContent, { CreateTaskData } from './create-task-content'; import CreateTaskContent, { CreateTaskData } from './create-task-content';
interface Props { interface Props {
onCreate: (data: CreateTaskData) => void; onCreate: (data: CreateTaskData, onProgress?: (status: string) => void) => Promise<any>;
status: string;
error: string;
taskId: number | null;
installedGit: boolean; installedGit: boolean;
dumpers: [] dumpers: []
} }
export default function CreateTaskPage(props: Props): JSX.Element { export default function CreateTaskPage(props: Props): JSX.Element {
const { const {
error, status, taskId, onCreate, installedGit, dumpers, onCreate, installedGit, dumpers,
} = props; } = props;
const location = useLocation(); const location = useLocation();
const [error, setError] = useState('');
let projectId = null; let projectId = null;
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
if (params.get('projectId')?.match(/^[1-9]+[0-9]*$/)) { if (params.get('projectId')?.match(/^[1-9]+[0-9]*$/)) {
projectId = +(params.get('projectId') as string); 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(() => { useEffect(() => {
if (error) { if (error) {
@ -75,12 +80,11 @@ export default function CreateTaskPage(props: Props): JSX.Element {
<Col md={20} lg={16} xl={14} xxl={9}> <Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Create a new task</Text> <Text className='cvat-title'>Create a new task</Text>
<CreateTaskContent <CreateTaskContent
taskId={taskId}
projectId={projectId} projectId={projectId}
status={status} onCreate={handleCreate}
onCreate={onCreate}
installedGit={installedGit} installedGit={installedGit}
dumpers={dumpers} dumpers={dumpers}
many={many}
/> />
</Col> </Col>
</Row> </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) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -9,17 +10,17 @@ import Tabs from 'antd/lib/tabs';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import Paragraph from 'antd/lib/typography/Paragraph'; 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 Empty from 'antd/lib/empty';
import Tree, { TreeNodeNormal } from 'antd/lib/tree/Tree'; import Tree, { TreeNodeNormal } from 'antd/lib/tree/Tree';
import { FormInstance } from 'antd/lib/form'; import { FormInstance } from 'antd/lib/form';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { EventDataNode } from 'rc-tree/lib/interface'; import { EventDataNode } from 'rc-tree/lib/interface';
import { InboxOutlined } from '@ant-design/icons';
import consts from 'consts'; import consts from 'consts';
import { CloudStorage } from 'reducers'; import { CloudStorage } from 'reducers';
import CloudStorageTab from './cloud-storages-tab'; import CloudStorageTab from './cloud-storages-tab';
import LocalFiles from './local-files';
export interface Files { export interface Files {
local: File[]; local: File[];
@ -37,9 +38,15 @@ interface State {
} }
interface Props { interface Props {
treeData: TreeNodeNormal[]; treeData: (TreeNodeNormal & { mime_type: string })[];
onLoadData: (key: string, success: () => void, failure: () => void) => void; share: any;
many: boolean;
onLoadData: (key: string) => Promise<any>;
onChangeActiveKey(key: string): void; 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> { export class FileManager extends React.PureComponent<Props, State> {
@ -48,6 +55,7 @@ export class FileManager extends React.PureComponent<Props, State> {
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
this.cloudStorageTabFormRef = React.createRef<FormInstance>(); this.cloudStorageTabFormRef = React.createRef<FormInstance>();
const { onLoadData } = this.props;
this.state = { this.state = {
files: { files: {
@ -62,17 +70,19 @@ export class FileManager extends React.PureComponent<Props, State> {
active: 'local', active: 'local',
}; };
this.loadData('/'); onLoadData('/');
} }
private onSelectCloudStorageFiles = (cloudStorageFiles: string[]): void => { private handleUploadCloudStorageFiles = (cloudStorageFiles: string[]): void => {
const { files } = this.state; const { files } = this.state;
const { onUploadCloudStorageFiles } = this.props;
this.setState({ this.setState({
files: { files: {
...files, ...files,
cloudStorage: cloudStorageFiles, cloudStorage: cloudStorageFiles,
}, },
}); });
onUploadCloudStorageFiles(cloudStorageFiles);
}; };
public getCloudStorageId(): number | null { 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 { public reset(): void {
const { active } = this.state; const { active } = this.state;
if (active === 'cloudStorage') { if (active === 'cloudStorage') {
@ -118,65 +120,43 @@ export class FileManager extends React.PureComponent<Props, State> {
} }
private renderLocalSelector(): JSX.Element { private renderLocalSelector(): JSX.Element {
const { many, onUploadLocalFiles } = this.props;
const { files } = this.state; const { files } = this.state;
return ( return (
<Tabs.TabPane className='cvat-file-manager-local-tab' key='local' tab='My computer'> <Tabs.TabPane className='cvat-file-manager-local-tab' key='local' tab='My computer'>
<Upload.Dragger <LocalFiles
multiple files={files.local}
listType='text' many={many}
fileList={files.local as any[]} onUpload={(_: RcFile, newLocalFiles: RcFile[]): boolean => {
showUploadList={
files.local.length < 5 && {
showRemoveIcon: false,
}
}
beforeUpload={(_: RcFile, newLocalFiles: RcFile[]): boolean => {
this.setState({ this.setState({
files: { files: {
...files, ...files,
local: newLocalFiles, local: newLocalFiles,
}, },
}); });
onUploadLocalFiles(newLocalFiles);
return false; 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> </Tabs.TabPane>
); );
} }
private renderShareSelector(): JSX.Element { private renderShareSelector(): JSX.Element {
function renderTreeNodes(data: TreeNodeNormal[]): JSX.Element[] { function getTreeNodes(data: TreeNodeNormal[]): TreeNodeNormal[] {
// sort alphabetically // sort alphabetically
data.sort((a: TreeNodeNormal, b: TreeNodeNormal): number => ( return data
a.key.toLocaleString().localeCompare(b.key.toLocaleString()))); .sort((a: TreeNodeNormal, b: TreeNodeNormal): number => (
return data.map((item: TreeNodeNormal) => { a.key.toLocaleString().localeCompare(b.key.toLocaleString())))
if (item.children) { .map((it) => ({
return ( ...it,
<Tree.TreeNode title={item.title} key={item.key} data={item} isLeaf={item.isLeaf}> children: it.children ? getTreeNodes(it.children) : undefined,
{renderTreeNodes(item.children)} }));
</Tree.TreeNode>
);
}
return <Tree.TreeNode {...item} key={item.key} data={item} />;
});
} }
const { SHARE_MOUNT_GUIDE_URL } = consts; const { SHARE_MOUNT_GUIDE_URL } = consts;
const { treeData } = this.props; const { treeData, onUploadShareFiles, onLoadData } = this.props;
const { expandedKeys, files } = this.state; const { expandedKeys, files } = this.state;
return ( return (
@ -190,7 +170,7 @@ export class FileManager extends React.PureComponent<Props, State> {
checkStrictly={false} checkStrictly={false}
expandedKeys={expandedKeys} expandedKeys={expandedKeys}
checkedKeys={files.share} 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 => { onExpand={(newExpandedKeys: ReactText[]): void => {
this.setState({ this.setState({
expandedKeys: newExpandedKeys.map((text: ReactText): string => text.toLocaleString()), expandedKeys: newExpandedKeys.map((text: ReactText): string => text.toLocaleString()),
@ -212,10 +192,10 @@ export class FileManager extends React.PureComponent<Props, State> {
share: keys, share: keys,
}, },
}); });
onUploadShareFiles(keys).then().catch();
}} }}
> treeData={getTreeNodes(treeData)}
{renderTreeNodes(treeData)} />
</Tree>
) : ( ) : (
<div className='cvat-empty-share-tree'> <div className='cvat-empty-share-tree'>
<Empty /> <Empty />
@ -233,6 +213,7 @@ export class FileManager extends React.PureComponent<Props, State> {
} }
private renderRemoteSelector(): JSX.Element { private renderRemoteSelector(): JSX.Element {
const { onUploadRemoteFiles } = this.props;
const { files } = this.state; const { files } = this.state;
return ( return (
@ -243,12 +224,14 @@ export class FileManager extends React.PureComponent<Props, State> {
rows={6} rows={6}
value={[...files.remote].join('\n')} value={[...files.remote].join('\n')}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>): void => { onChange={(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
const urls = event.target.value.split('\n');
this.setState({ this.setState({
files: { files: {
...files, ...files,
remote: event.target.value.split('\n'), remote: urls,
}, },
}); });
onUploadRemoteFiles(urls.filter(Boolean));
}} }}
/> />
</Tabs.TabPane> </Tabs.TabPane>
@ -274,14 +257,14 @@ export class FileManager extends React.PureComponent<Props, State> {
setSearchPhrase={(_potentialCloudStorage: string) => { setSearchPhrase={(_potentialCloudStorage: string) => {
this.setState({ potentialCloudStorage: _potentialCloudStorage }); this.setState({ potentialCloudStorage: _potentialCloudStorage });
}} }}
onSelectFiles={this.onSelectCloudStorageFiles} onSelectFiles={this.handleUploadCloudStorageFiles}
/> />
</Tabs.TabPane> </Tabs.TabPane>
); );
} }
public render(): JSX.Element { public render(): JSX.Element {
const { onChangeActiveKey } = this.props; const { onChangeActiveKey, many } = this.props;
const { active } = this.state; const { active } = this.state;
return ( return (
@ -300,7 +283,7 @@ export class FileManager extends React.PureComponent<Props, State> {
{this.renderLocalSelector()} {this.renderLocalSelector()}
{this.renderShareSelector()} {this.renderShareSelector()}
{this.renderRemoteSelector()} {this.renderRemoteSelector()}
{this.renderCloudStorageSelector()} {!many && this.renderCloudStorageSelector()}
</Tabs> </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 { import {
localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config,
} from './tasks-filter-configuration'; } from './tasks-filter-configuration';
import { MutliPlusIcon } from '../../icons';
const FilteringComponent = ResourceFilterHOC( const FilteringComponent = ResourceFilterHOC(
config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues,
@ -101,6 +102,14 @@ export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element
> >
Create a new task Create a new task
</Button> </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 <Button
className='cvat-import-task-button' className='cvat-import-task-button'
type='primary' type='primary'

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

@ -1,39 +1,53 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { uniqBy } from 'lodash';
import { TreeNodeNormal } from 'antd/lib/tree/Tree'; import { TreeNodeNormal } from 'antd/lib/tree/Tree';
import FileManagerComponent, { Files } from 'components/file-manager/file-manager'; import FileManagerComponent, { Files } from 'components/file-manager/file-manager';
import { loadShareDataAsync } from 'actions/share-actions'; import { loadShareDataAsync } from 'actions/share-actions';
import { ShareItem, CombinedState } from 'reducers'; import { ShareItem, CombinedState, ShareFileInfo } from 'reducers';
interface OwnProps { interface OwnProps {
ref: any; ref: any;
many: boolean
onChangeActiveKey(key: string): void; 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 { interface StateToProps {
treeData: TreeNodeNormal[]; treeData: (TreeNodeNormal & { mime_type: string })[];
share: any;
} }
interface DispatchToProps { interface DispatchToProps {
getTreeData(key: string, success: () => void, failure: () => void): void; getTreeData(key: string): Promise<ShareFileInfo[]>;
} }
function mapStateToProps(state: CombinedState): StateToProps { 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( return items.map(
(item): TreeNodeNormal => { (item): (TreeNodeNormal & { mime_type: string }) => {
const isLeaf = item.type !== 'DIR'; const isLeaf = item.type !== 'DIR';
const key = `${path}${item.name}${isLeaf ? '' : '/'}`; const key = `${path}${item.name}${isLeaf ? '' : '/'}`;
return { return {
key, key,
isLeaf, isLeaf,
title: item.name || 'root', title: item.name || 'root',
mime_type: item.mime_type,
children: convert(item.children, key), children: convert(item.children, key),
}; };
}, },
@ -41,16 +55,17 @@ function mapStateToProps(state: CombinedState): StateToProps {
} }
const { root } = state.share; const { root } = state.share;
return { return {
treeData: convert([root], ''), treeData: convert([root], ''),
share: state.share,
}; };
} }
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
getTreeData: (key: string, success: () => void, failure: () => void): void => { getTreeData: (key: string):
dispatch(loadShareDataAsync(key, success, failure)); Promise<ShareFileInfo[]> => dispatch(loadShareDataAsync(key)),
},
}; };
} }
@ -65,6 +80,63 @@ export class FileManagerContainer extends React.PureComponent<Props> {
this.managerComponentRef = React.createRef(); 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 { public getFiles(): Files {
return this.managerComponentRef.getFiles(); return this.managerComponentRef.getFiles();
} }
@ -78,12 +150,27 @@ export class FileManagerContainer extends React.PureComponent<Props> {
} }
public render(): JSX.Element { public render(): JSX.Element {
const { treeData, getTreeData, onChangeActiveKey } = this.props; const {
treeData,
share,
getTreeData,
many,
onChangeActiveKey,
onUploadLocalFiles,
onUploadRemoteFiles,
onUploadCloudStorageFiles,
} = this.props;
return ( return (
<FileManagerComponent <FileManagerComponent
treeData={treeData} treeData={treeData}
share={share}
many={many}
onLoadData={getTreeData} onLoadData={getTreeData}
onUploadLocalFiles={onUploadLocalFiles}
onUploadRemoteFiles={onUploadRemoteFiles}
onUploadShareFiles={this.handleUploadShareFiles}
onUploadCloudStorageFiles={onUploadCloudStorageFiles}
onChangeActiveKey={onChangeActiveKey} onChangeActiveKey={onChangeActiveKey}
ref={(component): void => { ref={(component): void => {
this.managerComponentRef = component; this.managerComponentRef = component;

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // 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 SVGCVATS3Provider from './assets/S3.svg';
import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg'; import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg';
import SVGRestoreIcon from './assets/restore-icon.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 CVATLogo = React.memo((): JSX.Element => <SVGCVATLogo />);
export const CursorIcon = React.memo((): JSX.Element => <SVGCursorIcon />); 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 S3Provider = React.memo((): JSX.Element => <SVGCVATS3Provider />);
export const GoogleCloudProvider = React.memo((): JSX.Element => <SVGCVATGoogleCloudProvider />); export const GoogleCloudProvider = React.memo((): JSX.Element => <SVGCVATGoogleCloudProvider />);
export const RestoreIcon = React.memo((): JSX.Element => <SVGRestoreIcon />); export const RestoreIcon = React.memo((): JSX.Element => <SVGRestoreIcon />);
export const MutliPlusIcon = React.memo((): JSX.Element => <SVGMultiPlusIcon />);

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

@ -33,11 +33,6 @@ const defaultState: TasksState = {
}, },
activities: { activities: {
deletes: {}, deletes: {},
creates: {
taskId: null,
status: '',
error: '',
},
jobUpdates: {}, 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: { case TasksActionTypes.UPDATE_TASK: {
return { return {
...state, ...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): class FileInfoSerializer(serializers.Serializer):
name = serializers.CharField(max_length=1024) name = serializers.CharField(max_length=1024)
type = serializers.ChoiceField(choices=["REG", "DIR"]) type = serializers.ChoiceField(choices=["REG", "DIR"])
mime_type = serializers.CharField(max_length=255)
class LogEventSerializer(serializers.Serializer): class LogEventSerializer(serializers.Serializer):
job_id = serializers.IntegerField(required=False) 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.frame_provider import FrameProvider
from cvat.apps.engine.media_extractors import ImageListReader from cvat.apps.engine.media_extractors import ImageListReader
from cvat.apps.engine.mime_types import mimetypes from cvat.apps.engine.mime_types import mimetypes
from cvat.apps.engine.media_extractors import get_mime
from cvat.apps.engine.models import ( from cvat.apps.engine.models import (
Job, Task, Project, Issue, Data, Job, Task, Project, Issue, Data,
Comment, StorageMethodChoice, StorageChoice, Image, Comment, StorageMethodChoice, StorageChoice, Image,
@ -186,13 +187,20 @@ class ServerViewSet(viewsets.ViewSet):
content = os.scandir(directory) content = os.scandir(directory)
for entry in content: for entry in content:
entry_type = None entry_type = None
entry_mime_type = None
if entry.is_file(): if entry.is_file():
entry_type = "REG" entry_type = "REG"
entry_mime_type = get_mime(os.path.join(settings.SHARE_ROOT, entry))
elif entry.is_dir(): elif entry.is_dir():
entry_type = "DIR" entry_type = "DIR"
entry_mime_type = "DIR"
if entry_type: 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) serializer = FileInfoSerializer(many=True, data=data)
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):

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

Loading…
Cancel
Save