From 3a1a591a3daa717e9416684f4247aae61d1cd7fa Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 26 Nov 2019 18:23:46 +0300 Subject: [PATCH 01/15] Automatic label matching (by the same name) in model running window --- .../model-runner-modal/model-runner-modal.tsx | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx index aeb0439306b2..42ea7cf93b21 100644 --- a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx +++ b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx @@ -14,12 +14,14 @@ import { import { Model } from '../../reducers/interfaces'; +interface StringObject { + [index: string]: string; +} + interface Props { modelsInitialized: boolean; models: Model[]; - activeProcesses: { - [index: string]: string; - }; + activeProcesses: StringObject; visible: boolean; taskInstance: any; startingError: string; @@ -28,9 +30,7 @@ interface Props { runInference( taskInstance: any, model: Model, - mapping: { - [index: string]: string - }, + mapping: StringObject, cleanOut: boolean, ): void; } @@ -38,12 +38,8 @@ interface Props { interface State { selectedModel: string | null; cleanOut: boolean; - mapping: { - [index: string]: string; - }; - colors: { - [index: string]: string; - }; + mapping: StringObject; + colors: StringObject; matching: { model: string, task: string, @@ -277,7 +273,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent model.name === this.state.selectedModel)[0]; + if (!model.primary) { + let taskLabels: string[] = this.props.taskInstance.labels + .map((label: any) => label.name); + const defaultMapping: StringObject = model.labels + .reduce((acc: StringObject, label) => { + if (taskLabels.includes(label)) { + acc[label] = label; + taskLabels = taskLabels.filter((_label) => _label !== label) + } + + return acc; + }, {}); + + this.setState({ + mapping: defaultMapping, + }); + } + } } public componentDidMount() { From 1f5f8a9c5ce92419aa84e356910510f655559c51 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 26 Nov 2019 19:21:02 +0300 Subject: [PATCH 02/15] Improved create task window --- .../create-model-page/create-model-form.tsx | 2 +- .../advanced-configuration-form.tsx | 131 +++++++++--------- .../basic-configuration-form.tsx | 3 +- .../create-task-page/create-task-content.tsx | 9 +- .../create-task-page/create-task-page.tsx | 2 +- .../components/file-manager/file-manager.tsx | 1 - 6 files changed, 73 insertions(+), 75 deletions(-) diff --git a/cvat-ui/src/components/create-model-page/create-model-form.tsx b/cvat-ui/src/components/create-model-page/create-model-form.tsx index 44eb7b23ceed..81966a9359e0 100644 --- a/cvat-ui/src/components/create-model-page/create-model-form.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-form.tsx @@ -50,7 +50,7 @@ export class CreateModelForm extends React.PureComponent { - + { getFieldDecorator('name', { rules: [{ required: true, diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx index 93c100ca4422..56cc88da846a 100644 --- a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx @@ -58,28 +58,25 @@ class AdvancedConfigurationForm extends React.PureComponent { private renderZOrder() { return ( - - - {this.props.form.getFieldDecorator('zOrder', { - initialValue: false, - valuePropName: 'checked', - })( - - - Z-order - - - )} - + + {this.props.form.getFieldDecorator('zOrder', { + initialValue: false, + valuePropName: 'checked', + })( + + + Z-order + + + )} ); } private renderImageQuality() { return ( - + - {'Image quality'} {this.props.form.getFieldDecorator('imageQuality', { initialValue: 70, rules: [{ @@ -102,9 +99,8 @@ class AdvancedConfigurationForm extends React.PureComponent { private renderOverlap() { return ( - + - {'Overlap size'} {this.props.form.getFieldDecorator('overlapSize')( )} @@ -115,9 +111,8 @@ class AdvancedConfigurationForm extends React.PureComponent { private renderSegmentSize() { return ( - + - {'Segment size'} {this.props.form.getFieldDecorator('segmentSize')( )} @@ -128,8 +123,7 @@ class AdvancedConfigurationForm extends React.PureComponent { private renderStartFrame() { return ( - - {'Start frame'} + {this.props.form.getFieldDecorator('startFrame')( { private renderStopFrame() { return ( - - {'Stop frame'} + {this.props.form.getFieldDecorator('stopFrame')( { private renderFrameStep() { return ( - - {'Frame step'} + {this.props.form.getFieldDecorator('frameStep')( { private renderGitLFSBox() { return ( - - - {this.props.form.getFieldDecorator('lfs', { - valuePropName: 'checked', - initialValue: false, - })( - - - Use LFS (Large File Support) - - - )} - + + {this.props.form.getFieldDecorator('lfs', { + valuePropName: 'checked', + initialValue: false, + })( + + + Use LFS (Large File Support): + + + )} ); } private renderGitRepositoryURL() { return ( - - - {'Dataset repository URL'} - {this.props.form.getFieldDecorator('repository', { - rules: [{ - validator: (_, value, callback) => { + + {this.props.form.getFieldDecorator('repository', { + rules: [{ + validator: (_, value, callback) => { + if (!value) { + callback(); + } else { const [url, path] = value.split(/\s+/); if (!patterns.validateURL.pattern.test(url)) { callback('Git URL is not a valid'); @@ -213,14 +207,13 @@ class AdvancedConfigurationForm extends React.PureComponent { callback(); } - }] - })( - - )} - + } + }] + })( + + )} ); } @@ -231,7 +224,6 @@ class AdvancedConfigurationForm extends React.PureComponent { {this.renderGitRepositoryURL()} - @@ -245,19 +237,24 @@ class AdvancedConfigurationForm extends React.PureComponent { private renderBugTracker() { return ( - - - {'Issue tracker'} - {this.props.form.getFieldDecorator('bugTracker', { - rules: [{ - ...patterns.validateURL, - }] - })( - - )} - + + {this.props.form.getFieldDecorator('bugTracker', { + rules: [{ + validator: (_, value, callback) => { + if (value && !patterns.validateURL.pattern.test(value)) { + callback('Issue tracker must be URL'); + } else { + callback(); + } + } + }] + })( + + )} ) } diff --git a/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx index 2d0d5d0a656a..e0b877a45072 100644 --- a/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx @@ -39,8 +39,7 @@ class BasicConfigurationForm extends React.PureComponent { const { getFieldDecorator } = this.props.form; return (
e.preventDefault()}> - Name - + { getFieldDecorator('name', { rules: [{ required: true, diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 9b4ac355d873..c67452ac2c50 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -137,7 +137,8 @@ export default class CreateTaskContent extends React.PureComponent private renderLabelsBlock() { return ( - Labels + * + Labels: private renderFilesBlock() { return ( + * + Select files: this.fileManagerContainer = container @@ -169,7 +172,7 @@ export default class CreateTaskContent extends React.PureComponent {'Advanced configuration'} + Advanced configuration } key='1'> return ( - {'Basic configuration'} + Basic configuration { this.renderBasicBlock() } diff --git a/cvat-ui/src/components/create-task-page/create-task-page.tsx b/cvat-ui/src/components/create-task-page/create-task-page.tsx index d615584399a9..d59d5f3615ab 100644 --- a/cvat-ui/src/components/create-task-page/create-task-page.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-page.tsx @@ -20,7 +20,7 @@ export default function CreateTaskPage(props: Props) { return ( - {'Create a new task'} + Create a new task { public render() { return ( <> - {'Select files'} this.setState({ active: activeKey as any, })}> From 27b0b5f127db923f78d9ed322d76057161180cf6 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 26 Nov 2019 19:35:13 +0300 Subject: [PATCH 03/15] Imroved upload model window --- .../create-model-page/create-model-content.tsx | 6 ++++++ .../create-model-page/create-model-form.tsx | 12 ++++-------- .../create-model-page/create-model-page.tsx | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cvat-ui/src/components/create-model-page/create-model-content.tsx b/cvat-ui/src/components/create-model-page/create-model-content.tsx index 94c36e111cda..e2da3337130b 100644 --- a/cvat-ui/src/components/create-model-page/create-model-content.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-content.tsx @@ -11,6 +11,8 @@ import { message, } from 'antd'; +import Text from 'antd/lib/typography/Text'; + import CreateModelForm, { CreateModelForm as WrappedCreateModelForm } from './create-model-form'; @@ -116,6 +118,10 @@ export default class CreateModelContent extends React.PureComponent { } /> + + * + Select files: + diff --git a/cvat-ui/src/components/create-model-page/create-model-form.tsx b/cvat-ui/src/components/create-model-page/create-model-form.tsx index 81966a9359e0..1a1965933468 100644 --- a/cvat-ui/src/components/create-model-page/create-model-form.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-form.tsx @@ -43,12 +43,11 @@ export class CreateModelForm extends React.PureComponent { return ( e.preventDefault()}> - - - Name - - + + * + Name: + { getFieldDecorator('name', { @@ -74,9 +73,6 @@ export class CreateModelForm extends React.PureComponent { - - - ); } diff --git a/cvat-ui/src/components/create-model-page/create-model-page.tsx b/cvat-ui/src/components/create-model-page/create-model-page.tsx index 1f1e2e05a712..205e7fe91055 100644 --- a/cvat-ui/src/components/create-model-page/create-model-page.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-page.tsx @@ -21,7 +21,7 @@ export default function CreateModelPageComponent(props: Props) { return ( - {`Upload a new model`} + Upload a new model Date: Tue, 26 Nov 2019 19:53:52 +0300 Subject: [PATCH 04/15] Minor fixes --- .../components/create-model-page/create-model-content.tsx | 4 +++- cvat-ui/src/components/file-manager/file-manager.tsx | 8 ++++---- .../src/components/models-page/uploaded-model-item.tsx | 3 +-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cvat-ui/src/components/create-model-page/create-model-content.tsx b/cvat-ui/src/components/create-model-page/create-model-content.tsx index e2da3337130b..60ab1dd90ac1 100644 --- a/cvat-ui/src/components/create-model-page/create-model-content.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-content.tsx @@ -108,7 +108,9 @@ export default class CreateModelContent extends React.PureComponent { - {window.open(guideLink, '_blank')}} type='question-circle'/> + { + window.open(guideLink, '_blank') + }} type='question-circle'/> diff --git a/cvat-ui/src/components/file-manager/file-manager.tsx b/cvat-ui/src/components/file-manager/file-manager.tsx index ba1df9d9f007..e34aeaba6673 100644 --- a/cvat-ui/src/components/file-manager/file-manager.tsx +++ b/cvat-ui/src/components/file-manager/file-manager.tsx @@ -81,13 +81,13 @@ export default class FileManager extends React.PureComponent { Support for a bulk images or a single video

- { this.state.files.local.length ? + { !!this.state.files.local.length && <>
- {this.state.files.local.length} file(s) selected + {`${this.state.files.local.length} file(s) selected`} - : null + } ); @@ -189,7 +189,7 @@ export default class FileManager extends React.PureComponent { })}> { this.renderLocalSelector() } { this.renderShareSelector() } - { this.props.withRemote ? this.renderRemoteSelector() : null } + { this.props.withRemote && this.renderRemoteSelector() }
); diff --git a/cvat-ui/src/components/models-page/uploaded-model-item.tsx b/cvat-ui/src/components/models-page/uploaded-model-item.tsx index 01d488973150..bad3f1a63cae 100644 --- a/cvat-ui/src/components/models-page/uploaded-model-item.tsx +++ b/cvat-ui/src/components/models-page/uploaded-model-item.tsx @@ -7,7 +7,6 @@ import { Select, Menu, Dropdown, - Button, Icon, } from 'antd'; @@ -62,7 +61,7 @@ export default function UploadedModelItem(props: Props) { Actions + { props.onDelete(); }}key='delete'>Delete From 37cc807753f5e975b18d1f0ef0a655ddb4874778 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 26 Nov 2019 20:16:55 +0300 Subject: [PATCH 05/15] Fixed: error window showed twice --- cvat-ui/src/components/task-page/details.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index 11cccc2b7fb5..6d2b0b2e5de3 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -209,12 +209,19 @@ export default class DetailsComponent extends React.PureComponent const { taskInstance } = this.props; const { bugTracker } = this.state; + let shown = false; const onChangeValue = (value: string) => { if (value && !patterns.validateURL.pattern.test(value)) { - Modal.error({ - title: `Could not update the task ${taskInstance.id}`, - content: 'Issue tracker is expected to be URL', - }); + if (!shown) { + Modal.error({ + title: `Could not update the task ${taskInstance.id}`, + content: 'Issue tracker is expected to be URL', + onOk: (() => { + shown = false; + }), + }); + shown = true; + } } else { this.setState({ bugTracker: value, From abf72519d163e55ecf37c196280fc0c325279f85 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 26 Nov 2019 20:20:03 +0300 Subject: [PATCH 06/15] Updated CONTRIBUTING.md --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2c6e88d7d29..ea7c4a631d5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,7 @@ $ python3 -m venv .env $ . .env/bin/activate $ pip install -U pip wheel $ pip install -r cvat/requirements/development.txt +$ pip install -r datumaro/requirements.txt $ python manage.py migrate $ python manage.py collectstatic ``` From 0d92d33e505c6da20948de21fda77606f160667d Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 27 Nov 2019 14:02:01 +0300 Subject: [PATCH 07/15] Removed token before login, fixed dump submenu (adjustment), fixed case when empty models list displayed --- cvat-core/src/server-proxy.js | 1 + cvat-ui/src/components/models-page/models-page.tsx | 11 +++++++---- cvat-ui/src/stylesheet.css | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 4faf9e21a43e..b151768473b3 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -147,6 +147,7 @@ `${encodeURIComponent('password')}=${encodeURIComponent(password)}`, ]).join('&').replace(/%20/g, '+'); + Axios.defaults.headers.common.Authorization = ''; let authenticationResponse = null; try { authenticationResponse = await Axios.post( diff --git a/cvat-ui/src/components/models-page/models-page.tsx b/cvat-ui/src/components/models-page/models-page.tsx index f6d3df5099a4..fe6fb1c277d1 100644 --- a/cvat-ui/src/components/models-page/models-page.tsx +++ b/cvat-ui/src/components/models-page/models-page.tsx @@ -35,15 +35,18 @@ export default function ModelsPageComponent(props: Props) { return (
- { integratedModels.length ? - : null } - { uploadedModels.length && + { !!integratedModels.length && + + } + { !!uploadedModels.length && - } { props.installedAutoAnnotation && + } + { props.installedAutoAnnotation && + !uploadedModels.length && !props.installedTFAnnotation && !props.installedTFSegmentation && diff --git a/cvat-ui/src/stylesheet.css b/cvat-ui/src/stylesheet.css index c170431dc9da..283aeae60b8d 100644 --- a/cvat-ui/src/stylesheet.css +++ b/cvat-ui/src/stylesheet.css @@ -316,6 +316,10 @@ background-color: rgba(24,144,255,0.05); } +.cvat-actions-menu-dump-submenu-item > button { + text-align: start; +} + .cvat-actions-menu-dump-submenu-item:hover { background-color: rgba(24,144,255,0.05); } From a29288f23a2b3e8f70e7504b559d472ae2a604b4 Mon Sep 17 00:00:00 2001 From: Nobody Date: Wed, 27 Nov 2019 11:07:18 +0000 Subject: [PATCH 08/15] Initial commit From c197c7baa124af106ec3f2dd6577d20cd4b2f2c0 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 27 Nov 2019 19:18:42 +0300 Subject: [PATCH 09/15] Export as dataset, better error showing system --- cvat-core/src/server-proxy.js | 2 +- cvat-ui/src/actions/formats-actions.ts | 16 +- cvat-ui/src/actions/notification-actions.ts | 24 ++ cvat-ui/src/actions/tasks-actions.ts | 63 +++- cvat-ui/src/actions/users-actions.ts | 11 - .../components/actions-menu/actions-menu.tsx | 28 +- .../components/actions-menu/dumper-item.tsx | 8 +- .../components/actions-menu/export-item.tsx | 38 +++ .../create-task-page/create-task-content.tsx | 16 +- .../create-task-page/create-task-page.tsx | 2 - cvat-ui/src/components/cvat-app.tsx | 166 ++++++--- cvat-ui/src/components/header/header.tsx | 130 +++---- .../src/components/login-page/login-page.tsx | 9 - .../model-runner-modal/model-runner-modal.tsx | 8 - .../components/models-page/models-page.tsx | 1 - .../register-page/register-page.tsx | 8 - .../src/components/task-page/task-page.tsx | 40 +-- .../src/components/tasks-page/tasks-page.tsx | 42 --- .../containers/actions-menu/actions-menu.tsx | 26 +- .../create-task-page/create-task-page.tsx | 3 - .../containers/file-manager/file-manager.tsx | 1 + cvat-ui/src/containers/header/header.tsx | 3 - .../src/containers/login-page/login-page.tsx | 14 +- .../model-runner-dialog.tsx | 3 - .../containers/models-page/models-page.tsx | 3 - .../register-page/register-page.tsx | 15 +- .../src/containers/task-page/task-page.tsx | 19 +- .../src/containers/tasks-page/tasks-page.tsx | 19 -- cvat-ui/src/index.tsx | 27 +- cvat-ui/src/reducers/auth-reducer.ts | 31 -- cvat-ui/src/reducers/formats-reducer.ts | 11 +- cvat-ui/src/reducers/interfaces.ts | 72 ++-- cvat-ui/src/reducers/models-reducer.ts | 36 -- cvat-ui/src/reducers/notifications-reducer.ts | 319 ++++++++++++++++++ cvat-ui/src/reducers/root-reducer.ts | 2 + cvat-ui/src/reducers/share-reducer.ts | 15 - cvat-ui/src/reducers/tasks-reducer.ts | 120 ++++--- cvat-ui/src/reducers/users-reducer.ts | 8 - cvat-ui/src/stylesheet.css | 8 + 39 files changed, 859 insertions(+), 508 deletions(-) create mode 100644 cvat-ui/src/actions/notification-actions.ts create mode 100644 cvat-ui/src/components/actions-menu/export-item.tsx create mode 100644 cvat-ui/src/reducers/notifications-reducer.ts diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index b151768473b3..fcd5a2a06ef7 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -247,7 +247,7 @@ try { await Axios.delete(`${backendAPI}/tasks/${id}`); } catch (errorData) { - throw generateError(errorData, 'Could not delete the task from the server'); + throw generateError(errorData, `Could not delete the task ${id} from the server`); } } diff --git a/cvat-ui/src/actions/formats-actions.ts b/cvat-ui/src/actions/formats-actions.ts index 806e73aa0f66..1168b7d9ae67 100644 --- a/cvat-ui/src/actions/formats-actions.ts +++ b/cvat-ui/src/actions/formats-actions.ts @@ -10,11 +10,15 @@ export enum FormatsActionTypes { GETTING_FORMATS_FAILED = 'GETTING_FORMATS_FAILED', } -export function gettingFormatsSuccess(formats: any): AnyAction { +export function gettingFormatsSuccess( + annotationFormats: any[], + datasetFormats: any[], +): AnyAction { return { type: FormatsActionTypes.GETTING_FORMATS_SUCCESS, payload: { - formats, + annotationFormats, + datasetFormats, }, }; } @@ -30,14 +34,16 @@ export function gettingFormatsFailed(error: any): AnyAction { export function gettingFormatsAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { - let formats = null; + let annotationFormats = null; + let datasetFormats = null; try { - formats = await cvat.server.formats(); + annotationFormats = await cvat.server.formats(); + datasetFormats = await cvat.server.datasetFormats(); } catch (error) { dispatch(gettingFormatsFailed(error)); return; } - dispatch(gettingFormatsSuccess(formats)); + dispatch(gettingFormatsSuccess(annotationFormats, datasetFormats)); }; } diff --git a/cvat-ui/src/actions/notification-actions.ts b/cvat-ui/src/actions/notification-actions.ts new file mode 100644 index 000000000000..b8431106c524 --- /dev/null +++ b/cvat-ui/src/actions/notification-actions.ts @@ -0,0 +1,24 @@ +import { AnyAction } from 'redux'; + +export enum NotificationsActionType { + RESET_ERRORS = 'RESET_ERRORS', + RESET_MESSAGES = 'RESET_MESSAGES', +} + +export function resetErrors(): AnyAction { + const action = { + type: NotificationsActionType.RESET_ERRORS, + payload: {}, + }; + + return action; +} + +export function resetMessages(): AnyAction { + const action = { + type: NotificationsActionType.RESET_MESSAGES, + payload: {}, + }; + + return action; +} diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index b54378234ed2..911c8e1d0357 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -16,6 +16,9 @@ export enum TasksActionTypes { DUMP_ANNOTATIONS = 'DUMP_ANNOTATIONS', DUMP_ANNOTATIONS_SUCCESS = 'DUMP_ANNOTATIONS_SUCCESS', DUMP_ANNOTATIONS_FAILED = 'DUMP_ANNOTATIONS_FAILED', + EXPORT_DATASET = 'EXPORT_DATASET', + EXPORT_DATASET_SUCCESS = 'EXPORT_DATASET_SUCCESS', + EXPORT_DATASET_FAILED = 'EXPORT_DATASET_FAILED', DELETE_TASK = 'DELETE_TASK', DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS', DELETE_TASK_FAILED = 'DELETE_TASK_FAILED', @@ -26,6 +29,7 @@ export enum TasksActionTypes { UPDATE_TASK = 'UPDATE_TASK', UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS', UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED', + RESET_ERROR = 'RESET_ERROR', } function getTasks(): AnyAction { @@ -150,7 +154,9 @@ ThunkAction, {}, {}, AnyAction> { try { dispatch(dumpAnnotation(task, dumper)); const url = await task.annotations.dump(task.name, dumper); - window.location.assign(url); + // false positive + // eslint-disable-next-line security/detect-non-literal-fs-filename + window.open(url); } catch (error) { dispatch(dumpAnnotationFailed(task, dumper, error)); return; @@ -210,6 +216,61 @@ ThunkAction, {}, {}, AnyAction> { }; } +function exportDataset(task: any, exporter: any): AnyAction { + const action = { + type: TasksActionTypes.EXPORT_DATASET, + payload: { + task, + exporter, + }, + }; + + return action; +} + +function exportDatasetSuccess(task: any, exporter: any): AnyAction { + const action = { + type: TasksActionTypes.EXPORT_DATASET_SUCCESS, + payload: { + task, + exporter, + }, + }; + + return action; +} + +function exportDatasetFailed(task: any, exporter: any, error: any): AnyAction { + const action = { + type: TasksActionTypes.EXPORT_DATASET_FAILED, + payload: { + task, + exporter, + error, + }, + }; + + return action; +} + +export function exportDatasetAsync(task: any, exporter: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + dispatch(exportDataset(task, exporter)); + + try { + const url = await task.annotations.exportDataset(task.name, exporter); + // false positive + // eslint-disable-next-line security/detect-non-literal-fs-filename + window.open(url, '_blank'); + } catch (error) { + dispatch(exportDatasetFailed(task, exporter, error)); + } + + dispatch(exportDatasetSuccess(task, exporter)); + }; +} + function deleteTask(taskID: number): AnyAction { const action = { type: TasksActionTypes.DELETE_TASK, diff --git a/cvat-ui/src/actions/users-actions.ts b/cvat-ui/src/actions/users-actions.ts index c64cf111ed81..1ea373052c83 100644 --- a/cvat-ui/src/actions/users-actions.ts +++ b/cvat-ui/src/actions/users-actions.ts @@ -6,20 +6,10 @@ import getCore from '../core'; const core = getCore(); export enum UsersActionTypes { - GET_USERS = 'GET_USERS', GET_USERS_SUCCESS = 'GET_USERS_SUCCESS', GET_USERS_FAILED = 'GET_USERS_FAILED', } -function getUsers(): AnyAction { - const action = { - type: UsersActionTypes.GET_USERS, - payload: { }, - }; - - return action; -} - function getUsersSuccess(users: any[]): AnyAction { const action = { type: UsersActionTypes.GET_USERS_SUCCESS, @@ -42,7 +32,6 @@ export function getUsersAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { - dispatch(getUsers()); const users = await core.users.get(); dispatch( getUsersSuccess( diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index c782aaf22a65..9b0003569a6f 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -5,24 +5,27 @@ import { Modal, } from 'antd'; -import Text from 'antd/lib/typography/Text'; import { ClickParam } from 'antd/lib/menu/index'; import LoaderItemComponent from './loader-item'; import DumperItemComponent from './dumper-item'; +import ExportItemComponent from './export-item'; interface ActionsMenuComponentProps { taskInstance: any; loaders: any[]; dumpers: any[]; + exporters: any[]; loadActivity: string | null; dumpActivities: string[] | null; + exportActivities: string[] | null; installedTFAnnotation: boolean; installedTFSegmentation: boolean; installedAutoAnnotation: boolean; onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void; onDumpAnnotation: (taskInstance: any, dumper: any) => void; + onExportDataset: (taskInstance: any, exporter: any) => void; onDeleteTask: (taskInstance: any) => void; onOpenRunWindow: (taskInstance: any) => void; } @@ -66,24 +69,22 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) { const tracker = props.taskInstance.bugTracker; const renderModelRunner = props.installedAutoAnnotation || props.installedTFAnnotation || props.installedTFSegmentation; + return ( handleMenuClick(props, params) }> - {'Dump annotations'} - }> + { props.dumpers.map((dumper) => DumperItemComponent({ dumper, taskInstance: props.taskInstance, - dumpActivities: props.dumpActivities, + dumpActivity: (props.dumpActivities || []) + .filter((_dumper: string) => _dumper === dumper.name)[0] || null, onDumpAnnotation: props.onDumpAnnotation, } ))} - {'Upload annotations'} - }> + { props.loaders.map((loader) => LoaderItemComponent({ loader, @@ -93,6 +94,17 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) { })) } + + { + props.exporters.map((exporter) => ExportItemComponent({ + exporter, + taskInstance: props.taskInstance, + exportActivity: (props.exportActivities || []) + .filter((_exporter: string) => _exporter === exporter.name)[0] || null, + onExportDataset: props.onExportDataset, + })) + } + {tracker && Open bug tracker} {renderModelRunner && Automatic annotation}
diff --git a/cvat-ui/src/components/actions-menu/dumper-item.tsx b/cvat-ui/src/components/actions-menu/dumper-item.tsx index e9e9c36e4689..ca862a8ec327 100644 --- a/cvat-ui/src/components/actions-menu/dumper-item.tsx +++ b/cvat-ui/src/components/actions-menu/dumper-item.tsx @@ -11,7 +11,7 @@ import Text from 'antd/lib/typography/Text'; interface DumperItemComponentProps { taskInstance: any; dumper: any; - dumpActivities: string[] | null; + dumpActivity: string | null; onDumpAnnotation: (task: any, dumper: any) => void; } @@ -24,11 +24,7 @@ export default function DumperItemComponent(props: DumperItemComponentProps) { const task = props.taskInstance; const { mode } = task; const { dumper } = props; - - const dumpingWithThisDumper = (props.dumpActivities || []) - .filter((_dumper: string) => _dumper === dumper.name)[0]; - - const pending = !!dumpingWithThisDumper; + const pending = !!props.dumpActivity; return ( diff --git a/cvat-ui/src/components/actions-menu/export-item.tsx b/cvat-ui/src/components/actions-menu/export-item.tsx new file mode 100644 index 000000000000..157fd495b227 --- /dev/null +++ b/cvat-ui/src/components/actions-menu/export-item.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { + Menu, + Button, + Icon, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +interface DumperItemComponentProps { + taskInstance: any; + exporter: any; + exportActivity: string | null; + onExportDataset: (task: any, exporter: any) => void; +} + +export default function DumperItemComponent(props: DumperItemComponentProps) { + const task = props.taskInstance; + const { exporter } = props; + const pending = !!props.exportActivity; + + return ( + + + + ); +} + diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index c67452ac2c50..28d8f29c45c3 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -7,7 +7,7 @@ import { Modal, Button, Collapse, - message, + notification, } from 'antd'; import Text from 'antd/lib/typography/Text'; @@ -28,7 +28,6 @@ export interface CreateTaskData { interface Props { onCreate: (data: CreateTaskData) => void; status: string; - error: string; installedGit: boolean; } @@ -190,15 +189,10 @@ export default class CreateTaskContent extends React.PureComponent } public componentDidUpdate(prevProps: Props) { - if (this.props.error && prevProps.error !== this.props.error) { - Modal.error({ - title: 'Could not create task', - content: this.props.error, - }); - } - if (this.props.status === 'CREATED' && prevProps.status !== 'CREATED') { - message.success('The task has been created'); + notification.info({ + message: 'The task has been created', + }); this.basicConfigurationComponent.resetFields(); if (this.advancedConfigurationComponent) { @@ -216,7 +210,7 @@ export default class CreateTaskContent extends React.PureComponent public render() { const loading = !!this.props.status && this.props.status !== 'CREATED' - && !this.props.error; + && this.props.status !== 'FAILED'; return ( diff --git a/cvat-ui/src/components/create-task-page/create-task-page.tsx b/cvat-ui/src/components/create-task-page/create-task-page.tsx index d59d5f3615ab..2287ab5c51a2 100644 --- a/cvat-ui/src/components/create-task-page/create-task-page.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-page.tsx @@ -11,7 +11,6 @@ import CreateTaskContent, { CreateTaskData } from './create-task-content'; interface Props { onCreate: (data: CreateTaskData) => void; - error: string; status: string; installedGit: boolean; } @@ -23,7 +22,6 @@ export default function CreateTaskPage(props: Props) { Create a new task diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 6d7ad26f3f92..a1a8fcb9cf5e 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -1,12 +1,18 @@ import React from 'react'; - -import { Switch, Route, Redirect } from 'react-router'; -import { BrowserRouter } from 'react-router-dom'; - -import { Spin, Layout, Modal } from 'antd'; - import 'antd/dist/antd.css'; import '../stylesheet.css'; +import { BrowserRouter } from 'react-router-dom'; +import { + Switch, + Route, + Redirect, +} from 'react-router'; +import { + Spin, + Layout, + Icon, + notification, +} from 'antd'; import TasksPageContainer from '../containers/tasks-page/tasks-page'; import CreateTaskPageContainer from '../containers/create-task-page/create-task-page'; @@ -20,22 +26,23 @@ import HeaderContainer from '../containers/header/header'; import ModelRunnerModalContainer from '../containers/model-runner-dialog/model-runner-dialog'; import FeedbackComponent from './feedback'; +import { NotificationsState } from '../reducers/interfaces'; type CVATAppProps = { loadFormats: () => void; loadUsers: () => void; verifyAuthorized: () => void; initPlugins: () => void; + resetErrors: () => void; + resetMessages: () => void; pluginsInitialized: boolean; userInitialized: boolean; formatsInitialized: boolean; usersInitialized: boolean; - gettingAuthError: string; - gettingFormatsError: string; - gettingUsersError: string; installedAutoAnnotation: boolean; installedTFAnnotation: boolean; installedTFSegmentation: boolean; + notifications: NotificationsState; user: any; } @@ -44,54 +51,137 @@ export default class CVATApplication extends React.PureComponent { super(props); } + private showMessages() { + function showMessage(title: string) { + notification.info({ + message: title, + duration: null, + }); + } + + const { tasks } = this.props.notifications.messages; + let shown = !!tasks.loading; + + if (tasks.loading) { + showMessage(tasks.loading); + } + + if (shown) { + this.props.resetMessages(); + } + } + + private showErrors() { + function showError(title: string, _error: any) { + const error = _error.toString(); + notification.error({ + message: title, + duration: null, + description: error.length > 200 ? '' : error, + }); + + console.error(error); + } + + const { auth } = this.props.notifications.errors; + const { tasks } = this.props.notifications.errors; + const { formats } = this.props.notifications.errors; + const { users } = this.props.notifications.errors; + const { share } = this.props.notifications.errors; + const { models } = this.props.notifications.errors; + + let shown = !!auth.authorized || !!auth.login || !!auth.logout || !!auth.register + || !!tasks.fetching || !!tasks.updating || !!tasks.dumping || !!tasks.loading + || !!tasks.exporting || !!tasks.deleting || !!tasks.creating || !!formats.fetching + || !!users.fetching || !!share.fetching || !!models.creating || !!models.starting + || !!models.fetching || !!models.deleting || !!models.inferenceStatusFetching; + + if (auth.authorized) { + showError('Could not check authorization on the server', auth.authorized); + } + if (auth.login) { + showError('Could not login on the server', auth.login); + } + if (auth.register) { + showError('Could not register on the server', auth.register); + } + if (auth.logout) { + showError('Could not logout on the server', auth.logout); + } + if (tasks.fetching) { + showError('Could not fetch tasks from the server', tasks.fetching); + } + if (tasks.updating) { + showError('Could not update task on the server', tasks.updating); + } + if (tasks.dumping) { + showError('Could not dump annotations from the server', tasks.dumping); + } + if (tasks.loading) { + showError('Could not upload annotations to the server', tasks.loading); + } + if (tasks.exporting) { + showError('Could not export task from the server', tasks.exporting); + } + if (tasks.deleting) { + showError('Could not delete task on the server', tasks.deleting); + } + if (tasks.creating) { + showError('Could not create task on the server', tasks.creating); + } + if (formats.fetching) { + showError('Could not get annotations and dataset formats from the server', formats.fetching); + } + if (users.fetching) { + showError('Could not get users from the server', users.fetching); + } + if (share.fetching) { + showError('Could not get share info from the server', share.fetching); + } + if (models.creating) { + showError('Could not create model on the server', models.creating); + } + if (models.starting) { + showError('Could not run model on the server', models.starting); + } + if (models.fetching) { + showError('Could not get models from the server', models.fetching); + } + if (models.deleting) { + showError('Could not delete model from the server', models.deleting); + } + if (models.inferenceStatusFetching) { + showError('Could not fetch inference status from the server', models.inferenceStatusFetching); + } + + if (shown) { + this.props.resetErrors(); + } + } + public componentDidMount() { this.props.verifyAuthorized(); } public componentDidUpdate() { - if (!this.props.userInitialized || - this.props.userInitialized && this.props.user == null) { - return; - } + this.showErrors(); + this.showMessages(); - if (this.props.gettingAuthError) { - Modal.error({ - title: 'Could not check authorization', - content: `${this.props.gettingAuthError}`, - }); + if (!this.props.userInitialized || this.props.user == null) { + // not authorized user return; } if (!this.props.formatsInitialized) { this.props.loadFormats(); - return; - } - - if (this.props.gettingFormatsError) { - Modal.error({ - title: 'Could not receive annotations formats', - content: `${this.props.gettingFormatsError}`, - }); - return; } if (!this.props.usersInitialized) { this.props.loadUsers(); - return; - } - - if (this.props.gettingUsersError) { - Modal.error({ - title: 'Could not receive users', - content: `${this.props.gettingUsersError}`, - }); - - return; } if (!this.props.pluginsInitialized) { this.props.initPlugins(); - return; } } diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 5f04b5ab58e0..0efeb86b117f 100644 --- a/cvat-ui/src/components/header/header.tsx +++ b/cvat-ui/src/components/header/header.tsx @@ -8,7 +8,6 @@ import { Icon, Button, Menu, - Modal, } from 'antd'; import Text from 'antd/lib/typography/Text'; @@ -24,89 +23,70 @@ interface HeaderContainerProps { installedTFAnnotation: boolean; installedTFSegmentation: boolean; username: string; - logoutError: string; } type Props = HeaderContainerProps & RouteComponentProps; -class HeaderContainer extends React.PureComponent { - private cvatLogo: React.FunctionComponent; - private userLogo: React.FunctionComponent; +const cvatLogo = () => ; +const userLogo = () => ; - public constructor(props: Props) { - super(props); - this.cvatLogo = () => ; - this.userLogo = () => ; - } +function HeaderContainer(props: Props) { + const renderModels = props.installedAutoAnnotation + || props.installedTFAnnotation + || props.installedTFSegmentation; + return ( + +
+ - public componentDidUpdate(prevProps: Props) { - if (!prevProps.logoutError && this.props.logoutError) { - Modal.error({ - title: 'Could not logout', - content: `${this.props.logoutError}`, - }); - } - } - - public render() { - const { props } = this; - const renderModels = props.installedAutoAnnotation - || props.installedTFAnnotation - || props.installedTFSegmentation; - return ( - -
- - - - { renderModels ? - : null - } - { props.installedAnalytics ? - : null - } -
-
+ + { renderModels ? + : null + } + { props.installedAnalytics ? - - - { + const serverHost = core.config.backendAPI.slice(0, -7); + window.open(`${serverHost}/analytics/app/kibana`, '_blank'); + } + }> Analytics : null + } +
+
+ + + + + - - - - {props.username.length > 14 ? `${props.username.slice(0, 10)} ...` : props.username} - - - + + {props.username.length > 14 ? `${props.username.slice(0, 10)} ...` : props.username} + + - }> - Logout - - -
-
- ); - } + + }> + Logout + +
+
+ + ); } export default withRouter(HeaderContainer); diff --git a/cvat-ui/src/components/login-page/login-page.tsx b/cvat-ui/src/components/login-page/login-page.tsx index 96d76da3fbf2..fb2befa0b2b2 100644 --- a/cvat-ui/src/components/login-page/login-page.tsx +++ b/cvat-ui/src/components/login-page/login-page.tsx @@ -8,13 +8,11 @@ import Text from 'antd/lib/typography/Text'; import { Col, Row, - Modal, } from 'antd'; import LoginForm, { LoginData } from './login-form'; interface LoginPageComponentProps { - loginError: string; onLogin: (username: string, password: string) => void; } @@ -27,13 +25,6 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps xl: { span: 4 }, } - if (props.loginError) { - Modal.error({ - title: 'Could not login', - content: props.loginError, - }); - } - return ( diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx index 42ea7cf93b21..7898b452d121 100644 --- a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx +++ b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx @@ -24,7 +24,6 @@ interface Props { activeProcesses: StringObject; visible: boolean; taskInstance: any; - startingError: string; getModels(): void; closeDialog(): void; runInference( @@ -286,13 +285,6 @@ export default class ModelRunnerModalComponent extends React.PureComponent model.name === this.state.selectedModel)[0]; diff --git a/cvat-ui/src/components/models-page/models-page.tsx b/cvat-ui/src/components/models-page/models-page.tsx index fe6fb1c277d1..f6264176ac63 100644 --- a/cvat-ui/src/components/models-page/models-page.tsx +++ b/cvat-ui/src/components/models-page/models-page.tsx @@ -15,7 +15,6 @@ interface Props { installedTFSegmentation: boolean; installedTFAnnotation: boolean; modelsAreBeingFetched: boolean; - modelsFetchingError: any; registeredUsers: any[]; models: Model[]; getModels(): void; diff --git a/cvat-ui/src/components/register-page/register-page.tsx b/cvat-ui/src/components/register-page/register-page.tsx index 5605789e4dd5..d38c023478c4 100644 --- a/cvat-ui/src/components/register-page/register-page.tsx +++ b/cvat-ui/src/components/register-page/register-page.tsx @@ -14,7 +14,6 @@ import { import RegisterForm, { RegisterData } from '../../components/register-page/register-form'; interface RegisterPageComponentProps { - registerError: string; onRegister: (username: string, firstName: string, lastName: string, email: string, password1: string, password2: string) => void; @@ -29,13 +28,6 @@ function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponen xl: { span: 4 }, } - if (props.registerError) { - Modal.error({ - title: 'Could not register', - content: props.registerError, - }); - } - return ( diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index ada1fa5c0f67..aceb9512d00d 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -6,7 +6,6 @@ import { Col, Row, Spin, - Modal, } from 'antd'; import TopBarComponent from './top-bar'; @@ -15,10 +14,7 @@ import JobListContainer from '../../containers/task-page/job-list'; import { Task } from '../../reducers/interfaces'; interface TaskPageComponentProps { - task: Task; - taskFetchingError: string; - taskUpdatingError: string; - taskDeletingError: string; + task: Task | undefined | null; deleteActivity: boolean | null; installedGit: boolean; onFetchTask: (tid: number) => void; @@ -31,51 +27,29 @@ class TaskPageComponent extends React.PureComponent { if (this.props.deleteActivity) { this.props.history.replace('/tasks'); } - - const { id } = this.props.match.params; - - if (this.props.taskFetchingError) { - Modal.error({ - title: `Could not receive the task ${id}`, - content: this.props.taskFetchingError, - }); - } - - if (this.props.taskUpdatingError) { - Modal.error({ - title: `Could not update the task ${id}`, - content: this.props.taskUpdatingError, - }); - } - - if (this.props.taskDeletingError) { - Modal.error({ - title: `Could not delete the task ${id}`, - content: this.props.taskDeletingError, - }); - } } public render() { const { id } = this.props.match.params; - const fetchTask = !this.props.task && !this.props.taskFetchingError; + const fetchTask = !this.props.task && !(typeof(this.props.task) === 'undefined'); if (fetchTask) { this.props.onFetchTask(+id); return ( ); - } else if (this.props.taskFetchingError) { + } else if (typeof(this.props.task) === 'undefined') { return (
) } else { + const task = this.props.task as Task; return ( - - - + + + ); diff --git a/cvat-ui/src/components/tasks-page/tasks-page.tsx b/cvat-ui/src/components/tasks-page/tasks-page.tsx index 424620cab207..06f0f651ba9e 100644 --- a/cvat-ui/src/components/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/components/tasks-page/tasks-page.tsx @@ -16,11 +16,6 @@ import EmptyListComponent from './empty-list'; import TaskListContainer from '../../containers/tasks-page/tasks-list'; interface TasksPageProps { - deletingError: string; - dumpingError: string; - loadingError: string; - tasksFetchingError: string; - loadingDoneMessage: string; tasksAreBeingFetched: boolean; gettingQuery: TasksQuery; numberOfTasks: number; @@ -137,43 +132,6 @@ class TasksPageComponent extends React.PureComponent void; onDumpAnnotation: (taskInstance: any, dumper: any) => void; + onExportDataset: (taskInstance: any, exporter: any) => void; onDeleteTask: (taskInstance: any) => void; onOpenRunWindow: (taskInstance: any) => void; } function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { const { formats } = state; - const { dumps } = state.tasks.activities; - const { loads } = state.tasks.activities; + const { activities } = state.tasks; + const { dumps } = activities; + const { loads } = activities; + const _exports = activities.exports; const { plugins } = state.plugins; const id = own.taskInstance.id; @@ -43,10 +49,14 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { installedTFSegmentation: plugins.TF_SEGMENTATION, installedAutoAnnotation: plugins.AUTO_ANNOTATION, dumpActivities: dumps.byTask[id] ? dumps.byTask[id] : null, + exportActivities: _exports.byTask[id] ? _exports.byTask[id] : null, loadActivity: loads.byTask[id] ? loads.byTask[id] : null, - loaders: formats.loaders, - dumpers: formats.dumpers, - }; + loaders: formats.annotationFormats + .map((format: any): any[] => format.loaders).flat(), + dumpers: formats.annotationFormats + .map((format: any): any[] => format.dumpers).flat(), + exporters: formats.datasetFormats, + }; } function mapDispatchToProps(dispatch: any): DispatchToProps { @@ -57,6 +67,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onDumpAnnotation: (taskInstance: any, dumper: any) => { dispatch(dumpAnnotationsAsync(taskInstance, dumper)); }, + onExportDataset: (taskInstance: any, exporter: any) => { + dispatch(exportDatasetAsync(taskInstance, exporter)); + }, onDeleteTask: (taskInstance: any) => { dispatch(deleteTaskAsync(taskInstance)); }, @@ -72,13 +85,16 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps) taskInstance={props.taskInstance} loaders={props.loaders} dumpers={props.dumpers} + exporters={props.exporters} loadActivity={props.loadActivity} dumpActivities={props.dumpActivities} + exportActivities={props.exportActivities} installedTFAnnotation={props.installedTFAnnotation} installedTFSegmentation={props.installedTFSegmentation} installedAutoAnnotation={props.installedAutoAnnotation} onLoadAnnotation={props.onLoadAnnotation} onDumpAnnotation={props.onDumpAnnotation} + onExportDataset={props.onExportDataset} onDeleteTask={props.onDeleteTask} onOpenRunWindow={props.onOpenRunWindow} /> diff --git a/cvat-ui/src/containers/create-task-page/create-task-page.tsx b/cvat-ui/src/containers/create-task-page/create-task-page.tsx index 2deb946116de..a7655dce34ea 100644 --- a/cvat-ui/src/containers/create-task-page/create-task-page.tsx +++ b/cvat-ui/src/containers/create-task-page/create-task-page.tsx @@ -7,7 +7,6 @@ import { CreateTaskData } from '../../components/create-task-page/create-task-co import { createTaskAsync } from '../../actions/tasks-actions'; interface StateToProps { - creatingError: string; status: string; installedGit: boolean; } @@ -27,14 +26,12 @@ function mapStateToProps(state: CombinedState): StateToProps { return { ...creates, installedGit: state.plugins.plugins.GIT_INTEGRATION, - creatingError: creates.creatingError ? creates.creatingError.toString() : '', }; } function CreateTaskPageContainer(props: StateToProps & DispatchToProps) { return ( ); } diff --git a/cvat-ui/src/containers/login-page/login-page.tsx b/cvat-ui/src/containers/login-page/login-page.tsx index c1c6e13ac0dc..76974d2afda7 100644 --- a/cvat-ui/src/containers/login-page/login-page.tsx +++ b/cvat-ui/src/containers/login-page/login-page.tsx @@ -1,21 +1,16 @@ import React from 'react'; import { connect } from 'react-redux'; import { loginAsync } from '../../actions/auth-actions'; -import { CombinedState } from '../../reducers/interfaces'; import LoginPageComponent from '../../components/login-page/login-page'; -interface StateToProps { - loginError: any; -} +interface StateToProps {} interface DispatchToProps { login(username: string, password: string): void; } -function mapStateToProps(state: CombinedState): StateToProps { - return { - loginError: state.auth.loginError, - }; +function mapStateToProps(): StateToProps { + return {}; } function mapDispatchToProps(dispatch: any): DispatchToProps { @@ -24,11 +19,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { }; } -function LoginPageContainer(props: StateToProps & DispatchToProps) { +function LoginPageContainer(props: DispatchToProps) { return ( ); } diff --git a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx index 8fa8ef0d4ee6..233f1b4ae72c 100644 --- a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx +++ b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx @@ -14,7 +14,6 @@ import { interface StateToProps { - startingError: any; modelsInitialized: boolean; models: Model[]; activeProcesses: { @@ -46,7 +45,6 @@ function mapStateToProps(state: CombinedState): StateToProps { activeProcesses: {}, taskInstance: models.activeRunTask, visible: models.visibleRunWindows, - startingError: models.startingError, }; } @@ -82,7 +80,6 @@ function ModelRunnerModalContainer(props: StateToProps & DispatchToProps) { getModels={props.getModels} closeDialog={props.closeDialog} runInference={props.inferModelAsync} - startingError={props.startingError ? props.startingError.toString() : ''} /> ); } diff --git a/cvat-ui/src/containers/models-page/models-page.tsx b/cvat-ui/src/containers/models-page/models-page.tsx index e10cff23ca27..65f54ab025ab 100644 --- a/cvat-ui/src/containers/models-page/models-page.tsx +++ b/cvat-ui/src/containers/models-page/models-page.tsx @@ -16,7 +16,6 @@ interface StateToProps { installedTFAnnotation: boolean; installedTFSegmentation: boolean; modelsAreBeingFetched: boolean; - modelsFetchingError: any; models: Model[]; registeredUsers: any[]; } @@ -35,7 +34,6 @@ function mapStateToProps(state: CombinedState): StateToProps { installedTFAnnotation: plugins.TF_ANNOTATION, installedTFSegmentation: plugins.TF_SEGMENTATION, modelsAreBeingFetched: !models.initialized, - modelsFetchingError: models.fetchingError, models: models.models, registeredUsers: state.users.users, }; @@ -64,7 +62,6 @@ function ModelsPageContainer(props: DispatchToProps & StateToProps) { installedTFSegmentation={props.installedTFSegmentation} installedTFAnnotation={props.installedTFAnnotation} modelsAreBeingFetched={props.modelsAreBeingFetched} - modelsFetchingError={props.modelsFetchingError} registeredUsers={props.registeredUsers} models={props.models} getModels={props.getModels} diff --git a/cvat-ui/src/containers/register-page/register-page.tsx b/cvat-ui/src/containers/register-page/register-page.tsx index bdd7f230ac3d..b4e8a3054e36 100644 --- a/cvat-ui/src/containers/register-page/register-page.tsx +++ b/cvat-ui/src/containers/register-page/register-page.tsx @@ -1,12 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; import { registerAsync } from '../../actions/auth-actions'; -import { CombinedState } from '../../reducers/interfaces'; import RegisterPageComponent from '../../components/register-page/register-page'; -interface StateToProps { - registerError: any; -} +interface StateToProps {} interface DispatchToProps { register: (username: string, firstName: string, @@ -14,10 +11,8 @@ interface DispatchToProps { password1: string, password2: string) => void; } -function mapStateToProps(state: CombinedState): StateToProps { - return { - registerError: state.auth.registerError, - }; +function mapStateToProps(): StateToProps { + return {}; } function mapDispatchToProps(dispatch: any): DispatchToProps { @@ -26,11 +21,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { } } -type RegisterPageContainerProps = StateToProps & DispatchToProps; -function RegisterPageContainer(props: RegisterPageContainerProps) { +function RegisterPageContainer(props: StateToProps & DispatchToProps) { return ( ); diff --git a/cvat-ui/src/containers/task-page/task-page.tsx b/cvat-ui/src/containers/task-page/task-page.tsx index bc29944ff4dc..5aafa643bdf3 100644 --- a/cvat-ui/src/containers/task-page/task-page.tsx +++ b/cvat-ui/src/containers/task-page/task-page.tsx @@ -14,10 +14,7 @@ import { type Props = RouteComponentProps<{id: string}>; interface StateToProps { - task: Task; - taskFetchingError: any; - taskUpdatingError: any; - taskDeletingError: any; + task: Task | undefined | null; deleteActivity: boolean | null; installedGit: boolean; } @@ -29,11 +26,15 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState, own: Props): StateToProps { const { plugins } = state.plugins; const { deletes } = state.tasks.activities; - const taskDeletingError = deletes.deletingError; const id = +own.match.params.id; const filtered = state.tasks.current.filter((task) => task.instance.id === id); - const task = filtered[0] || null; + let task = null; + if (filtered.length) { + task = filtered[0]; + } else if (state.notifications.errors.tasks.fetching) { + task = undefined; + } let deleteActivity = null; if (task && id in deletes.byTask) { @@ -42,9 +43,6 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps { return { task, - taskFetchingError: state.tasks.tasksFetchingError, - taskUpdatingError: state.tasks.taskUpdatingError, - taskDeletingError, deleteActivity, installedGit: plugins.GIT_INTEGRATION, }; @@ -71,9 +69,6 @@ function TaskPageContainer(props: StateToProps & DispatchToProps) { return ( void; loadUsers: () => void; initPlugins: () => void; + resetErrors: () => void; + resetMessages: () => void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -49,12 +56,10 @@ function mapStateToProps(state: CombinedState): StateToProps { userInitialized: auth.initialized, usersInitialized: users.initialized, formatsInitialized: formats.initialized, - gettingAuthError: auth.authError, - gettingUsersError: users.gettingUsersError, - gettingFormatsError: formats.gettingFormatsError, installedAutoAnnotation: plugins.plugins.AUTO_ANNOTATION, installedTFSegmentation: plugins.plugins.TF_SEGMENTATION, installedTFAnnotation: plugins.plugins.TF_ANNOTATION, + notifications: {...state.notifications}, user: auth.user, }; } @@ -65,6 +70,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { verifyAuthorized: (): void => dispatch(authorizedAsync()), initPlugins: (): void => dispatch(checkPluginsAsync()), loadUsers: (): void => dispatch(getUsersAsync()), + resetErrors: (): void => dispatch(resetErrors()), + resetMessages: (): void => dispatch(resetMessages()), }; } @@ -75,16 +82,16 @@ function reduxAppWrapper(props: StateToProps & DispatchToProps) { loadFormats={props.loadFormats} loadUsers={props.loadUsers} verifyAuthorized={props.verifyAuthorized} + resetErrors={props.resetErrors} + resetMessages={props.resetMessages} pluginsInitialized={props.pluginsInitialized} userInitialized={props.userInitialized} usersInitialized={props.usersInitialized} formatsInitialized={props.formatsInitialized} - gettingAuthError={props.gettingAuthError ? props.gettingAuthError.toString() : ''} - gettingFormatsError={props.gettingFormatsError ? props.gettingFormatsError.toString() : ''} - gettingUsersError={props.gettingUsersError ? props.gettingUsersError.toString() : ''} installedAutoAnnotation={props.installedAutoAnnotation} installedTFSegmentation={props.installedTFSegmentation} installedTFAnnotation={props.installedTFAnnotation} + notifications={props.notifications} user={props.user} /> ) diff --git a/cvat-ui/src/reducers/auth-reducer.ts b/cvat-ui/src/reducers/auth-reducer.ts index d4c9cc97d4de..b4cf915a1670 100644 --- a/cvat-ui/src/reducers/auth-reducer.ts +++ b/cvat-ui/src/reducers/auth-reducer.ts @@ -5,10 +5,6 @@ import { AuthState } from './interfaces'; const defaultState: AuthState = { initialized: false, - authError: null, - loginError: null, - logoutError: null, - registerError: null, user: null, }; @@ -19,48 +15,21 @@ export default (state = defaultState, action: AnyAction): AuthState => { ...state, initialized: true, user: action.payload.user, - authError: null, - }; - case AuthActionTypes.AUTHORIZED_FAILED: - return { - ...state, - initialized: true, - authError: action.payload.error, }; case AuthActionTypes.LOGIN_SUCCESS: return { ...state, user: action.payload.user, - loginError: null, - }; - case AuthActionTypes.LOGIN_FAILED: - return { - ...state, - user: null, - loginError: action.payload.error, }; case AuthActionTypes.LOGOUT_SUCCESS: return { ...state, user: null, - logoutError: null, - }; - case AuthActionTypes.LOGOUT_FAILED: - return { - ...state, - logoutError: action.payload.error, }; case AuthActionTypes.REGISTER_SUCCESS: return { ...state, user: action.payload.user, - registerError: null, - }; - case AuthActionTypes.REGISTER_FAILED: - return { - ...state, - user: null, - registerError: action.payload.error, }; default: return state; diff --git a/cvat-ui/src/reducers/formats-reducer.ts b/cvat-ui/src/reducers/formats-reducer.ts index 2bdc2f9219e2..ac9d9b62c6f5 100644 --- a/cvat-ui/src/reducers/formats-reducer.ts +++ b/cvat-ui/src/reducers/formats-reducer.ts @@ -4,9 +4,8 @@ import { FormatsActionTypes } from '../actions/formats-actions'; import { FormatsState } from './interfaces'; const defaultState: FormatsState = { - loaders: [], - dumpers: [], - gettingFormatsError: null, + annotationFormats: [], + datasetFormats: [], initialized: false, }; @@ -16,15 +15,13 @@ export default (state = defaultState, action: AnyAction): FormatsState => { return { ...state, initialized: true, - gettingFormatsError: null, - dumpers: action.payload.formats.map((format: any): any[] => format.dumpers).flat(), - loaders: action.payload.formats.map((format: any): any[] => format.loaders).flat(), + annotationFormats: action.payload.annotationFormats, + datasetFormats: action.payload.datasetFormats, }; case FormatsActionTypes.GETTING_FORMATS_FAILED: return { ...state, initialized: true, - gettingFormatsError: action.payload.error, }; default: return state; diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index bd51b400da67..52d5f469dcf1 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -1,9 +1,5 @@ export interface AuthState { initialized: boolean; - authError: any; - loginError: any; - logoutError: any; - registerError: any; user: any; } @@ -26,45 +22,43 @@ export interface Task { export interface TasksState { initialized: boolean; - tasksFetchingError: any; - taskUpdatingError: any; gettingQuery: TasksQuery; count: number; current: Task[]; activities: { dumps: { - dumpingError: any; byTask: { // dumps in different formats at the same time [tid: number]: string[]; // dumper names }; }; + exports: { + byTask: { + // exports in different formats at the same time + [tid: number]: string[]; // dumper names + }; + }; loads: { - loadingError: any; - loadingDoneMessage: string; byTask: { // only one loading simultaneously [tid: number]: string; // loader name }; }; deletes: { - deletingError: any; byTask: { [tid: number]: boolean; // deleted (deleting if in dictionary) }; }; creates: { - creatingError: any; status: string; }; }; } export interface FormatsState { - loaders: any[]; - dumpers: any[]; + annotationFormats: any[]; + datasetFormats: any[]; initialized: boolean; - gettingFormatsError: any; } // eslint-disable-next-line import/prefer-default-export @@ -86,7 +80,6 @@ export interface PluginsState { export interface UsersState { users: any[]; initialized: boolean; - gettingUsersError: any; } export interface ShareFileInfo { // get this data from cvat-core @@ -102,7 +95,6 @@ export interface ShareItem { export interface ShareState { root: ShareItem; - error: any; } export interface Model { @@ -126,12 +118,6 @@ export interface Running { export interface ModelsState { initialized: boolean; creatingStatus: string; - creatingError: any; - startingError: any; - fetchingError: any; - deletingErrors: { // by id - [index: number]: any; - }; models: Model[]; runnings: Running[]; visibleRunWindows: boolean; @@ -146,6 +132,47 @@ export interface ModelFiles { json: string | File; } +export interface NotificationsState { + errors: { + auth: { + authorized: any; + login: any; + logout: any; + register: any; + }; + tasks: { + fetching: any; + updating: any; + dumping: any; + loading: any; + exporting: any; + deleting: any; + creating: any; + }; + formats: { + fetching: any; + }; + users: { + fetching: any; + }; + share: { + fetching: any; + }; + models: { + creating: any; + starting: any; + fetching: any; + deleting: any; + inferenceStatusFetching: any; + }; + }; + messages: { + tasks: { + loading: string; + }; + }; +} + export interface CombinedState { auth: AuthState; tasks: TasksState; @@ -154,4 +181,5 @@ export interface CombinedState { formats: FormatsState; plugins: PluginsState; models: ModelsState; + notifications: NotificationsState; } diff --git a/cvat-ui/src/reducers/models-reducer.ts b/cvat-ui/src/reducers/models-reducer.ts index ec241ac530b8..199325cf5a4a 100644 --- a/cvat-ui/src/reducers/models-reducer.ts +++ b/cvat-ui/src/reducers/models-reducer.ts @@ -6,10 +6,6 @@ import { ModelsState } from './interfaces'; const defaultState: ModelsState = { initialized: false, creatingStatus: '', - creatingError: null, - startingError: null, - fetchingError: null, - deletingErrors: {}, models: [], visibleRunWindows: false, activeRunTask: null, @@ -21,7 +17,6 @@ export default function (state = defaultState, action: AnyAction): ModelsState { case ModelsActionTypes.GET_MODELS: { return { ...state, - fetchingError: null, initialized: false, }; } @@ -35,18 +30,9 @@ export default function (state = defaultState, action: AnyAction): ModelsState { case ModelsActionTypes.GET_MODELS_FAILED: { return { ...state, - fetchingError: action.payload.error, initialized: true, }; } - case ModelsActionTypes.DELETE_MODEL: { - const errors = { ...state.deletingErrors }; - delete errors[action.payload.id]; - return { - ...state, - deletingErrors: errors, - }; - } case ModelsActionTypes.DELETE_MODEL_SUCCESS: { return { ...state, @@ -55,18 +41,9 @@ export default function (state = defaultState, action: AnyAction): ModelsState { ), }; } - case ModelsActionTypes.DELETE_MODEL_FAILED: { - const errors = { ...state.deletingErrors }; - errors[action.payload.id] = action.payload.error; - return { - ...state, - deletingErrors: errors, - }; - } case ModelsActionTypes.CREATE_MODEL: { return { ...state, - creatingError: null, creatingStatus: '', }; } @@ -79,7 +56,6 @@ export default function (state = defaultState, action: AnyAction): ModelsState { case ModelsActionTypes.CREATE_MODEL_FAILED: { return { ...state, - creatingError: action.payload.error, creatingStatus: '', }; } @@ -90,18 +66,6 @@ export default function (state = defaultState, action: AnyAction): ModelsState { creatingStatus: 'CREATED', }; } - case ModelsActionTypes.INFER_MODEL: { - return { - ...state, - startingError: null, - }; - } - case ModelsActionTypes.INFER_MODEL_FAILED: { - return { - ...state, - startingError: action.payload.error, - }; - } case ModelsActionTypes.SHOW_RUN_MODEL_DIALOG: { return { ...state, diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts new file mode 100644 index 000000000000..cca896e8c890 --- /dev/null +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -0,0 +1,319 @@ +import { AnyAction } from 'redux'; + +import { AuthActionTypes } from '../actions/auth-actions'; +import { FormatsActionTypes } from '../actions/formats-actions'; +import { ModelsActionTypes } from '../actions/models-actions'; +import { ShareActionTypes } from '../actions/share-actions'; +import { TasksActionTypes } from '../actions/tasks-actions'; +import { UsersActionTypes } from '../actions/users-actions'; +import { NotificationsActionType } from '../actions/notification-actions'; + +import { NotificationsState } from './interfaces'; + +const defaultState: NotificationsState = { + errors: { + auth: { + authorized: null, + login: null, + logout: null, + register: null, + }, + tasks: { + fetching: null, + updating: null, + dumping: null, + loading: null, + exporting: null, + deleting: null, + creating: null, + }, + formats: { + fetching: null, + }, + users: { + fetching: null, + }, + share: { + fetching: null, + }, + models: { + creating: null, + starting: null, + fetching: null, + deleting: null, + inferenceStatusFetching: null, + }, + }, + messages: { + tasks: { + loading: '', + }, + }, +}; + +export default function (state = defaultState, action: AnyAction): NotificationsState { + switch (action.type) { + case AuthActionTypes.AUTHORIZED_FAILED: { + return { + ...state, + errors: { + ...state.errors, + auth: { + ...state.errors.auth, + authorized: action.payload.error, + }, + }, + }; + } + case AuthActionTypes.LOGIN_FAILED: { + return { + ...state, + errors: { + ...state.errors, + auth: { + ...state.errors.auth, + login: action.payload.error, + }, + }, + }; + } + case AuthActionTypes.LOGOUT_FAILED: { + return { + ...state, + errors: { + ...state.errors, + auth: { + ...state.errors.auth, + logout: action.payload.error, + }, + }, + }; + } + case AuthActionTypes.REGISTER_FAILED: { + return { + ...state, + errors: { + ...state.errors, + auth: { + ...state.errors.auth, + register: action.payload.error, + }, + }, + }; + } + case TasksActionTypes.EXPORT_DATASET_FAILED: { + return { + ...state, + errors: { + ...state.errors, + tasks: { + ...state.errors.tasks, + exporting: action.payload.error, + }, + }, + }; + } + case TasksActionTypes.GET_TASKS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + tasks: { + ...state.errors.tasks, + fetching: action.payload.error, + }, + }, + }; + } + case TasksActionTypes.LOAD_ANNOTATIONS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + tasks: { + ...state.errors.tasks, + loading: action.payload.error, + }, + }, + }; + } + case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: { + const { task } = action.payload; + return { + ...state, + messages: { + ...state.messages, + tasks: { + ...state.messages.tasks, + loading: `Annotations have been loaded to the task ${task.id}`, + }, + }, + }; + } + case TasksActionTypes.UPDATE_TASK_FAILED: { + return { + ...state, + errors: { + ...state.errors, + tasks: { + ...state.errors.tasks, + updating: action.payload.error, + }, + }, + }; + } + case TasksActionTypes.DUMP_ANNOTATIONS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + tasks: { + ...state.errors.tasks, + dumping: action.payload.error, + }, + }, + }; + } + case TasksActionTypes.DELETE_TASK_FAILED: { + return { + ...state, + errors: { + ...state.errors, + tasks: { + ...state.errors.tasks, + deleting: action.payload.error, + }, + }, + }; + } + case TasksActionTypes.CREATE_TASK_FAILED: { + return { + ...state, + errors: { + ...state.errors, + tasks: { + ...state.errors.tasks, + creating: action.payload.error, + }, + }, + }; + } + case FormatsActionTypes.GETTING_FORMATS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + formats: { + ...state.errors.formats, + fetching: action.payload.error, + }, + }, + }; + } + case UsersActionTypes.GET_USERS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + users: { + ...state.errors.users, + fetching: action.payload.error, + }, + }, + }; + } + case ShareActionTypes.LOAD_SHARE_DATA_FAILED: { + return { + ...state, + errors: { + ...state.errors, + share: { + ...state.errors.share, + fetching: action.payload.error, + }, + }, + }; + } + case ModelsActionTypes.CREATE_MODEL_FAILED: { + return { + ...state, + errors: { + ...state.errors, + models: { + ...state.errors.models, + creating: action.payload.error, + }, + }, + }; + } + case ModelsActionTypes.DELETE_MODEL_FAILED: { + return { + ...state, + errors: { + ...state.errors, + models: { + ...state.errors.models, + deleting: action.payload.error, + }, + }, + }; + } + case ModelsActionTypes.GET_INFERENCE_STATUS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + models: { + ...state.errors.models, + inferenceStatusFetching: action.payload.error, + }, + }, + }; + } + case ModelsActionTypes.GET_MODELS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + models: { + ...state.errors.models, + fetching: action.payload.error, + }, + }, + }; + } + case ModelsActionTypes.INFER_MODEL_FAILED: { + return { + ...state, + errors: { + ...state.errors, + models: { + ...state.errors.models, + starting: action.payload.error, + }, + }, + }; + } + case NotificationsActionType.RESET_ERRORS: { + return { + ...state, + errors: { + ...defaultState.errors, + }, + }; + } + case NotificationsActionType.RESET_MESSAGES: { + return { + ...state, + messages: { + ...defaultState.messages, + }, + }; + } + default: { + return { + ...state, + }; + } + } +} diff --git a/cvat-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index d6073be0f752..52d0723b9212 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -6,6 +6,7 @@ import shareReducer from './share-reducer'; import formatsReducer from './formats-reducer'; import pluginsReducer from './plugins-reducer'; import modelsReducer from './models-reducer'; +import notificationsReducer from './notifications-reducer'; export default function createRootReducer(): Reducer { return combineReducers({ @@ -16,5 +17,6 @@ export default function createRootReducer(): Reducer { formats: formatsReducer, plugins: pluginsReducer, models: modelsReducer, + notifications: notificationsReducer, }); } diff --git a/cvat-ui/src/reducers/share-reducer.ts b/cvat-ui/src/reducers/share-reducer.ts index 6ce035e18c55..b85ce5622d03 100644 --- a/cvat-ui/src/reducers/share-reducer.ts +++ b/cvat-ui/src/reducers/share-reducer.ts @@ -9,17 +9,10 @@ const defaultState: ShareState = { type: 'DIR', children: [], }, - error: null, }; export default function (state = defaultState, action: AnyAction): ShareState { switch (action.type) { - case ShareActionTypes.LOAD_SHARE_DATA: { - return { - ...state, - error: null, - }; - } case ShareActionTypes.LOAD_SHARE_DATA_SUCCESS: { const { values } = action.payload; const { directory } = action.payload; @@ -45,14 +38,6 @@ export default function (state = defaultState, action: AnyAction): ShareState { ...state, }; } - case ShareActionTypes.LOAD_SHARE_DATA_FAILED: { - const { error } = action.payload; - - return { - ...state, - error, - }; - } default: return { ...state, diff --git a/cvat-ui/src/reducers/tasks-reducer.ts b/cvat-ui/src/reducers/tasks-reducer.ts index ea27bfa32468..b7a6c22ed6f7 100644 --- a/cvat-ui/src/reducers/tasks-reducer.ts +++ b/cvat-ui/src/reducers/tasks-reducer.ts @@ -5,8 +5,6 @@ import { TasksState, Task } from './interfaces'; const defaultState: TasksState = { initialized: false, - tasksFetchingError: null, - taskUpdatingError: null, count: 0, current: [], gettingQuery: { @@ -21,52 +19,24 @@ const defaultState: TasksState = { }, activities: { dumps: { - dumpingError: null, + byTask: {}, + }, + exports: { byTask: {}, }, loads: { - loadingError: null, - loadingDoneMessage: '', byTask: {}, }, deletes: { - deletingError: null, byTask: {}, }, creates: { - creatingError: null, status: '', }, }, }; -export default (inputState: TasksState = defaultState, action: AnyAction): TasksState => { - function cleanupTemporaryInfo(stateToResetErrors: TasksState): TasksState { - return { - ...stateToResetErrors, - tasksFetchingError: null, - taskUpdatingError: null, - activities: { - ...stateToResetErrors.activities, - dumps: { - ...stateToResetErrors.activities.dumps, - dumpingError: null, - }, - loads: { - ...stateToResetErrors.activities.loads, - loadingError: null, - loadingDoneMessage: '', - }, - deletes: { - ...stateToResetErrors.activities.deletes, - deletingError: null, - }, - }, - }; - } - - const state = cleanupTemporaryInfo(inputState); - +export default (state: TasksState = defaultState, action: AnyAction): TasksState => { switch (action.type) { case TasksActionTypes.GET_TASKS: return { @@ -74,7 +44,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks activities: { ...state.activities, deletes: { - deletingError: null, byTask: {}, }, }, @@ -102,7 +71,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks count: 0, current: [], gettingQuery: { ...action.payload.query }, - tasksFetchingError: action.payload.error, }; case TasksActionTypes.DUMP_ANNOTATIONS: { const { task } = action.payload; @@ -115,8 +83,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks const theTaskDumpingActivities = [...tasksDumpingActivities.byTask[task.id] || []]; if (!theTaskDumpingActivities.includes(dumper.name)) { theTaskDumpingActivities.push(dumper.name); - } else { - throw Error('Dump with the same dumper for this same task has been already started'); } tasksDumpingActivities.byTask[task.id] = theTaskDumpingActivities; @@ -152,11 +118,9 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks case TasksActionTypes.DUMP_ANNOTATIONS_FAILED: { const { task } = action.payload; const { dumper } = action.payload; - const dumpingError = action.payload.error; const tasksDumpingActivities = { ...state.activities.dumps, - dumpingError, }; const theTaskDumpingActivities = tasksDumpingActivities.byTask[task.id] @@ -172,6 +136,70 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks }, }; } + case TasksActionTypes.EXPORT_DATASET: { + const { task } = action.payload; + const { exporter } = action.payload; + + const tasksExportingActivities = { + ...state.activities.exports, + }; + + const theTaskDumpingActivities = [...tasksExportingActivities.byTask[task.id] || []]; + if (!theTaskDumpingActivities.includes(exporter.name)) { + theTaskDumpingActivities.push(exporter.name); + } + tasksExportingActivities.byTask[task.id] = theTaskDumpingActivities; + + return { + ...state, + activities: { + ...state.activities, + exports: tasksExportingActivities, + }, + }; + } + case TasksActionTypes.EXPORT_DATASET_SUCCESS: { + const { task } = action.payload; + const { exporter } = action.payload; + + const tasksExportingActivities = { + ...state.activities.exports, + }; + + const theTaskExportingActivities = tasksExportingActivities.byTask[task.id] + .filter((exporterName: string): boolean => exporterName !== exporter.name); + + tasksExportingActivities.byTask[task.id] = theTaskExportingActivities; + + return { + ...state, + activities: { + ...state.activities, + exports: tasksExportingActivities, + }, + }; + } + case TasksActionTypes.EXPORT_DATASET_FAILED: { + const { task } = action.payload; + const { exporter } = action.payload; + + const tasksExportingActivities = { + ...state.activities.exports, + }; + + const theTaskExportingActivities = tasksExportingActivities.byTask[task.id] + .filter((exporterName: string): boolean => exporterName !== exporter.name); + + tasksExportingActivities.byTask[task.id] = theTaskExportingActivities; + + return { + ...state, + activities: { + ...state.activities, + exports: tasksExportingActivities, + }, + }; + } case TasksActionTypes.LOAD_ANNOTATIONS: { const { task } = action.payload; const { loader } = action.payload; @@ -209,14 +237,12 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks ...state.activities, loads: { ...tasksLoadingActivity, - loadingDoneMessage: `Annotations have been loaded to the task ${task.id}`, }, }, }; } case TasksActionTypes.LOAD_ANNOTATIONS_FAILED: { const { task } = action.payload; - const loadingError = action.payload.error; const tasksLoadingActivity = { ...state.activities.loads, @@ -230,7 +256,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks ...state.activities, loads: { ...tasksLoadingActivity, - loadingError, }, }, }; @@ -273,7 +298,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks } case TasksActionTypes.DELETE_TASK_FAILED: { const { taskID } = action.payload; - const { error } = action.payload; const deletesActivities = state.activities.deletes; @@ -288,7 +312,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks ...state.activities, deletes: { ...deletesActivities, - deletingError: error, }, }, }; @@ -299,7 +322,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks activities: { ...state.activities, creates: { - creatingError: null, status: '', }, }, @@ -332,15 +354,13 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks }; } case TasksActionTypes.CREATE_TASK_FAILED: { - const { error } = action.payload; - return { ...state, activities: { ...state.activities, creates: { ...state.activities.creates, - creatingError: error, + status: 'FAILED', }, }, }; @@ -348,7 +368,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks case TasksActionTypes.UPDATE_TASK: { return { ...state, - taskUpdatingError: null, }; } case TasksActionTypes.UPDATE_TASK_SUCCESS: { @@ -369,7 +388,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks case TasksActionTypes.UPDATE_TASK_FAILED: { return { ...state, - taskUpdatingError: action.payload.error, current: state.current.map((task): Task => { if (task.instance.id === action.payload.taskInstance.id) { return { diff --git a/cvat-ui/src/reducers/users-reducer.ts b/cvat-ui/src/reducers/users-reducer.ts index cb821d1c8cac..f367b56ec65d 100644 --- a/cvat-ui/src/reducers/users-reducer.ts +++ b/cvat-ui/src/reducers/users-reducer.ts @@ -6,17 +6,10 @@ import { UsersActionTypes } from '../actions/users-actions'; const initialState: UsersState = { users: [], initialized: false, - gettingUsersError: null, }; export default function (state: UsersState = initialState, action: AnyAction): UsersState { switch (action.type) { - case UsersActionTypes.GET_USERS: - return { - ...state, - initialized: false, - gettingUsersError: null, - }; case UsersActionTypes.GET_USERS_SUCCESS: return { ...state, @@ -28,7 +21,6 @@ export default function (state: UsersState = initialState, action: AnyAction): U ...state, initialized: true, users: [], - gettingUsersError: action.payload.error, }; default: return { diff --git a/cvat-ui/src/stylesheet.css b/cvat-ui/src/stylesheet.css index 283aeae60b8d..1efae2742083 100644 --- a/cvat-ui/src/stylesheet.css +++ b/cvat-ui/src/stylesheet.css @@ -320,6 +320,14 @@ text-align: start; } +.cvat-actions-menu-export-submenu-item:hover { + background-color: rgba(24,144,255,0.05); +} + +.cvat-actions-menu-export-submenu-item > button { + text-align: start; +} + .cvat-actions-menu-dump-submenu-item:hover { background-color: rgba(24,144,255,0.05); } From 2e5a41ebe9f161a4b700d3446de4633406fde31b Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 27 Nov 2019 20:14:47 +0300 Subject: [PATCH 10/15] Removed extra requests, improved UI --- cvat-ui/src/actions/formats-actions.ts | 27 ++++++++++++------- cvat-ui/src/actions/plugins-actions.ts | 11 ++++++++ cvat-ui/src/actions/users-actions.ts | 12 +++++++++ .../create-model-content.tsx | 23 ++++++---------- .../create-model-page/create-model-page.tsx | 2 -- .../create-task-page/create-task-content.tsx | 19 +++++++------ cvat-ui/src/components/cvat-app.tsx | 12 +++++---- .../labels-editor/labels-editor.tsx | 8 +++--- .../components/models-page/models-page.tsx | 5 ++-- cvat-ui/src/components/task-page/details.tsx | 13 ++++----- .../src/components/task-page/task-page.tsx | 23 +++++++++++++--- .../create-model-page/create-model-page.tsx | 3 --- .../containers/file-manager/file-manager.tsx | 2 +- .../containers/models-page/models-page.tsx | 9 ++++--- .../src/containers/task-page/task-page.tsx | 12 ++++----- cvat-ui/src/index.tsx | 17 +++++++++--- cvat-ui/src/reducers/formats-reducer.ts | 14 ++++++++-- cvat-ui/src/reducers/interfaces.ts | 5 ++++ cvat-ui/src/reducers/models-reducer.ts | 4 +++ cvat-ui/src/reducers/notifications-reducer.ts | 2 +- cvat-ui/src/reducers/plugins-reducer.ts | 9 +++++++ cvat-ui/src/reducers/tasks-reducer.ts | 4 +++ cvat-ui/src/reducers/users-reducer.ts | 11 +++++++- cvat-ui/src/stylesheet.css | 2 +- 24 files changed, 170 insertions(+), 79 deletions(-) diff --git a/cvat-ui/src/actions/formats-actions.ts b/cvat-ui/src/actions/formats-actions.ts index 1168b7d9ae67..a3e74685a9d8 100644 --- a/cvat-ui/src/actions/formats-actions.ts +++ b/cvat-ui/src/actions/formats-actions.ts @@ -6,16 +6,24 @@ import getCore from '../core'; const cvat = getCore(); export enum FormatsActionTypes { - GETTING_FORMATS_SUCCESS = 'GETTING_FORMATS_SUCCESS', - GETTING_FORMATS_FAILED = 'GETTING_FORMATS_FAILED', + GET_FORMATS = 'GET_FORMATS', + GET_FORMATS_SUCCESS = 'GET_FORMATS_SUCCESS', + GET_FORMATS_FAILED = 'GET_FORMATS_FAILED', } -export function gettingFormatsSuccess( +function getFormats(): AnyAction { + return { + type: FormatsActionTypes.GET_FORMATS, + payload: {}, + }; +} + +function getFormatsSuccess( annotationFormats: any[], datasetFormats: any[], ): AnyAction { return { - type: FormatsActionTypes.GETTING_FORMATS_SUCCESS, + type: FormatsActionTypes.GET_FORMATS_SUCCESS, payload: { annotationFormats, datasetFormats, @@ -23,27 +31,28 @@ export function gettingFormatsSuccess( }; } -export function gettingFormatsFailed(error: any): AnyAction { +function getFormatsFailed(error: any): AnyAction { return { - type: FormatsActionTypes.GETTING_FORMATS_FAILED, + type: FormatsActionTypes.GET_FORMATS_FAILED, payload: { error, }, }; } -export function gettingFormatsAsync(): ThunkAction, {}, {}, AnyAction> { +export function getFormatsAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { + dispatch(getFormats()); let annotationFormats = null; let datasetFormats = null; try { annotationFormats = await cvat.server.formats(); datasetFormats = await cvat.server.datasetFormats(); } catch (error) { - dispatch(gettingFormatsFailed(error)); + dispatch(getFormatsFailed(error)); return; } - dispatch(gettingFormatsSuccess(annotationFormats, datasetFormats)); + dispatch(getFormatsSuccess(annotationFormats, datasetFormats)); }; } diff --git a/cvat-ui/src/actions/plugins-actions.ts b/cvat-ui/src/actions/plugins-actions.ts index 89c04d03afb7..0e27013fa4ff 100644 --- a/cvat-ui/src/actions/plugins-actions.ts +++ b/cvat-ui/src/actions/plugins-actions.ts @@ -4,6 +4,7 @@ import { SupportedPlugins } from '../reducers/interfaces'; import PluginChecker from '../utils/plugin-checker'; export enum PluginsActionTypes { + CHECK_PLUGINS = 'CHECK_PLUGINS', CHECKED_ALL_PLUGINS = 'CHECKED_ALL_PLUGINS' } @@ -11,6 +12,15 @@ interface PluginObjects { [plugin: string]: boolean; } +function checkPlugins(): AnyAction { + const action = { + type: PluginsActionTypes.CHECK_PLUGINS, + payload: {}, + }; + + return action; +} + function checkedAllPlugins(plugins: PluginObjects): AnyAction { const action = { type: PluginsActionTypes.CHECKED_ALL_PLUGINS, @@ -25,6 +35,7 @@ function checkedAllPlugins(plugins: PluginObjects): AnyAction { export function checkPluginsAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { + dispatch(checkPlugins()); const plugins: PluginObjects = {}; const promises: Promise[] = []; diff --git a/cvat-ui/src/actions/users-actions.ts b/cvat-ui/src/actions/users-actions.ts index 1ea373052c83..3d75197a5c28 100644 --- a/cvat-ui/src/actions/users-actions.ts +++ b/cvat-ui/src/actions/users-actions.ts @@ -6,10 +6,20 @@ import getCore from '../core'; const core = getCore(); export enum UsersActionTypes { + GET_USERS = 'GET_USERS', GET_USERS_SUCCESS = 'GET_USERS_SUCCESS', GET_USERS_FAILED = 'GET_USERS_FAILED', } +function getUsers(): AnyAction { + const action = { + type: UsersActionTypes.GET_USERS, + payload: {}, + }; + + return action; +} + function getUsersSuccess(users: any[]): AnyAction { const action = { type: UsersActionTypes.GET_USERS_SUCCESS, @@ -31,6 +41,8 @@ function getUsersFailed(error: any): AnyAction { export function getUsersAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { + dispatch(getUsers()); + try { const users = await core.users.get(); dispatch( diff --git a/cvat-ui/src/components/create-model-page/create-model-content.tsx b/cvat-ui/src/components/create-model-page/create-model-content.tsx index 60ab1dd90ac1..92719519c561 100644 --- a/cvat-ui/src/components/create-model-page/create-model-content.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-content.tsx @@ -9,6 +9,7 @@ import { Tooltip, Modal, message, + notification, } from 'antd'; import Text from 'antd/lib/typography/Text'; @@ -24,7 +25,6 @@ import { ModelFiles } from '../../reducers/interfaces'; interface Props { createModel(name: string, files: ModelFiles, global: boolean): void; isAdmin: boolean; - modelCreatingError: string; modelCreatingStatus: string; } @@ -66,17 +66,17 @@ export default class CreateModelContent extends React.PureComponent { if (Object.keys(grouppedFiles) .map((key: string) => grouppedFiles[key]) .filter((val) => !!val).length !== 4) { - Modal.error({ - title: 'Could not upload a model', - content: 'Please, specify correct files', + notification.error({ + message: 'Could not upload a model', + description: 'Please, specify correct files', }); } else { this.props.createModel(data.name, grouppedFiles, data.global); } }).catch(() => { - Modal.error({ - title: 'Could not upload a model', - content: 'Please, check input fields', + notification.error({ + message: 'Could not upload a model', + description: 'Please, check input fields', }); }) } @@ -88,13 +88,6 @@ export default class CreateModelContent extends React.PureComponent { this.modelForm.resetFields(); this.fileManagerContainer.reset(); } - - if (!prevProps.modelCreatingError && this.props.modelCreatingError) { - Modal.error({ - title: 'Could not create task', - content: this.props.modelCreatingError, - }); - } } public render() { @@ -128,7 +121,7 @@ export default class CreateModelContent extends React.PureComponent { this.fileManagerContainer = container - }/> + } withRemote={true}/> {status && } diff --git a/cvat-ui/src/components/create-model-page/create-model-page.tsx b/cvat-ui/src/components/create-model-page/create-model-page.tsx index 205e7fe91055..e58a8ff1c928 100644 --- a/cvat-ui/src/components/create-model-page/create-model-page.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-page.tsx @@ -13,7 +13,6 @@ import { ModelFiles } from '../../reducers/interfaces'; interface Props { createModel(name: string, files: ModelFiles, global: boolean): void; isAdmin: boolean; - modelCreatingError: string; modelCreatingStatus: string; } @@ -24,7 +23,6 @@ export default function CreateModelPageComponent(props: Props) { Upload a new model diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 28d8f29c45c3..57801a1b8ba8 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -4,7 +4,6 @@ import { Row, Col, Alert, - Modal, Button, Collapse, notification, @@ -89,17 +88,17 @@ export default class CreateTaskContent extends React.PureComponent private handleSubmitClick = () => { if (!this.validateLabels()) { - Modal.error({ - title: 'Could not create a task', - content: 'A task must contain at least one label', + notification.error({ + message: 'Could not create a task', + description: 'A task must contain at least one label', }); return; } if (!this.validateFiles()) { - Modal.error({ - title: 'Could not create a task', - content: 'A task must contain at least one file', + notification.error({ + message: 'Could not create a task', + description: 'A task must contain at least one file', }); return; } @@ -116,9 +115,9 @@ export default class CreateTaskContent extends React.PureComponent this.props.onCreate(this.state); }) .catch((_: any) => { - Modal.error({ - title: 'Could not create a task', - content: 'Please, check configuration you specified', + notification.error({ + message: 'Could not create a task', + description: 'Please, check configuration you specified', }); }); } diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index a1a8fcb9cf5e..275eb69df515 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -10,7 +10,6 @@ import { import { Spin, Layout, - Icon, notification, } from 'antd'; @@ -35,10 +34,13 @@ type CVATAppProps = { initPlugins: () => void; resetErrors: () => void; resetMessages: () => void; - pluginsInitialized: boolean; userInitialized: boolean; + pluginsInitialized: boolean; + pluginsFetching: boolean; formatsInitialized: boolean; + formatsFetching: boolean; usersInitialized: boolean; + usersFetching: boolean; installedAutoAnnotation: boolean; installedTFAnnotation: boolean; installedTFSegmentation: boolean; @@ -172,15 +174,15 @@ export default class CVATApplication extends React.PureComponent { return; } - if (!this.props.formatsInitialized) { + if (!this.props.formatsInitialized && !this.props.formatsFetching) { this.props.loadFormats(); } - if (!this.props.usersInitialized) { + if (!this.props.usersInitialized && !this.props.usersFetching) { this.props.loadUsers(); } - if (!this.props.pluginsInitialized) { + if (!this.props.pluginsInitialized && !this.props.pluginsFetching) { this.props.initPlugins(); } } diff --git a/cvat-ui/src/components/labels-editor/labels-editor.tsx b/cvat-ui/src/components/labels-editor/labels-editor.tsx index 6c7f24a1878a..f49b2e2236d0 100644 --- a/cvat-ui/src/components/labels-editor/labels-editor.tsx +++ b/cvat-ui/src/components/labels-editor/labels-editor.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Tabs, Icon, - Modal, + notification, } from 'antd'; import Text from 'antd/lib/typography/Text'; @@ -128,9 +128,9 @@ export default class LabelsEditor private handleDelete = (label: Label) => { // the label is saved on the server, cannot delete it if (typeof(label.id) !== 'undefined' && label.id >= 0) { - Modal.error({ - title: 'Could not delete the label', - content: 'It has been already saved on the server', + notification.error({ + message: 'Could not delete the label', + description: 'It has been already saved on the server', }); } diff --git a/cvat-ui/src/components/models-page/models-page.tsx b/cvat-ui/src/components/models-page/models-page.tsx index f6264176ac63..8db88abedf09 100644 --- a/cvat-ui/src/components/models-page/models-page.tsx +++ b/cvat-ui/src/components/models-page/models-page.tsx @@ -14,7 +14,8 @@ interface Props { installedAutoAnnotation: boolean; installedTFSegmentation: boolean; installedTFAnnotation: boolean; - modelsAreBeingFetched: boolean; + modelsInitialized: boolean; + modelsFetching: boolean; registeredUsers: any[]; models: Model[]; getModels(): void; @@ -22,7 +23,7 @@ interface Props { } export default function ModelsPageComponent(props: Props) { - if (props.modelsAreBeingFetched) { + if (!props.modelsInitialized && !props.modelsFetching) { props.getModels(); return ( diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index 6d2b0b2e5de3..355f2e279df9 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -7,6 +7,7 @@ import { Icon, Modal, Button, + notification, } from 'antd'; import Text from 'antd/lib/typography/Text'; @@ -287,9 +288,9 @@ export default class DetailsComponent extends React.PureComponent .then((data) => { if (data !== null && this.mounted) { if (data.status.error) { - Modal.error({ - title: 'Could not receive repository status', - content: data.status.error + notification.error({ + message: 'Could not receive repository status', + description: data.status.error }); } else { this.setState({ @@ -303,9 +304,9 @@ export default class DetailsComponent extends React.PureComponent } }).catch((error) => { if (this.mounted) { - Modal.error({ - title: 'Could not receive repository status', - content: error.toString(), + notification.error({ + message: 'Could not receive repository status', + description: error.toString(), }); } }); diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index aceb9512d00d..6e29066e5eb4 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -6,6 +6,7 @@ import { Col, Row, Spin, + notification, } from 'antd'; import TopBarComponent from './top-bar'; @@ -14,7 +15,8 @@ import JobListContainer from '../../containers/task-page/job-list'; import { Task } from '../../reducers/interfaces'; interface TaskPageComponentProps { - task: Task | undefined | null; + task: Task | null; + fetching: boolean; deleteActivity: boolean | null; installedGit: boolean; onFetchTask: (tid: number) => void; @@ -23,18 +25,33 @@ interface TaskPageComponentProps { type Props = TaskPageComponentProps & RouteComponentProps<{id: string}>; class TaskPageComponent extends React.PureComponent { + private attempts: number = 0; + public componentDidUpdate() { if (this.props.deleteActivity) { this.props.history.replace('/tasks'); } + + if (this.attempts > 1) { + notification.warning({ + message: 'Something wrong with the task. It cannot be fetched from the server', + }); + } } public render() { const { id } = this.props.match.params; - const fetchTask = !this.props.task && !(typeof(this.props.task) === 'undefined'); + const fetchTask = !this.props.task; if (fetchTask) { - this.props.onFetchTask(+id); + if (!this.props.fetching) { + if (!this.attempts) { + this.attempts ++; + this.props.onFetchTask(+id); + } else { + this.attempts ++; + } + } return ( ); diff --git a/cvat-ui/src/containers/create-model-page/create-model-page.tsx b/cvat-ui/src/containers/create-model-page/create-model-page.tsx index 1e47db0d60ba..188589f47ea6 100644 --- a/cvat-ui/src/containers/create-model-page/create-model-page.tsx +++ b/cvat-ui/src/containers/create-model-page/create-model-page.tsx @@ -10,7 +10,6 @@ import { interface StateToProps { isAdmin: boolean; - modelCreatingError: any; modelCreatingStatus: string; } @@ -23,7 +22,6 @@ function mapStateToProps(state: CombinedState): StateToProps { return { isAdmin: state.auth.user.isAdmin, - modelCreatingError: models.creatingError, modelCreatingStatus: models.creatingStatus, }; } @@ -40,7 +38,6 @@ function CreateModelPageContainer(props: StateToProps & DispatchToProps) { return ( diff --git a/cvat-ui/src/containers/file-manager/file-manager.tsx b/cvat-ui/src/containers/file-manager/file-manager.tsx index 4eb3ab018f41..9cdbd9ed9fd5 100644 --- a/cvat-ui/src/containers/file-manager/file-manager.tsx +++ b/cvat-ui/src/containers/file-manager/file-manager.tsx @@ -11,8 +11,8 @@ import { } from '../../reducers/interfaces'; interface OwnProps { - withRemote: boolean; ref: any; + withRemote: boolean; } interface StateToProps { diff --git a/cvat-ui/src/containers/models-page/models-page.tsx b/cvat-ui/src/containers/models-page/models-page.tsx index 65f54ab025ab..76c2e7f81b8a 100644 --- a/cvat-ui/src/containers/models-page/models-page.tsx +++ b/cvat-ui/src/containers/models-page/models-page.tsx @@ -15,7 +15,8 @@ interface StateToProps { installedAutoAnnotation: boolean; installedTFAnnotation: boolean; installedTFSegmentation: boolean; - modelsAreBeingFetched: boolean; + modelsInitialized: boolean; + modelsFetching: boolean; models: Model[]; registeredUsers: any[]; } @@ -33,7 +34,8 @@ function mapStateToProps(state: CombinedState): StateToProps { installedAutoAnnotation: plugins.AUTO_ANNOTATION, installedTFAnnotation: plugins.TF_ANNOTATION, installedTFSegmentation: plugins.TF_SEGMENTATION, - modelsAreBeingFetched: !models.initialized, + modelsInitialized: models.initialized, + modelsFetching: models.fetching, models: models.models, registeredUsers: state.users.users, }; @@ -61,7 +63,8 @@ function ModelsPageContainer(props: DispatchToProps & StateToProps) { installedAutoAnnotation={props.installedAutoAnnotation} installedTFSegmentation={props.installedTFSegmentation} installedTFAnnotation={props.installedTFAnnotation} - modelsAreBeingFetched={props.modelsAreBeingFetched} + modelsInitialized={props.modelsInitialized} + modelsFetching={props.modelsFetching} registeredUsers={props.registeredUsers} models={props.models} getModels={props.getModels} diff --git a/cvat-ui/src/containers/task-page/task-page.tsx b/cvat-ui/src/containers/task-page/task-page.tsx index 5aafa643bdf3..844df0844ff8 100644 --- a/cvat-ui/src/containers/task-page/task-page.tsx +++ b/cvat-ui/src/containers/task-page/task-page.tsx @@ -14,7 +14,8 @@ import { type Props = RouteComponentProps<{id: string}>; interface StateToProps { - task: Task | undefined | null; + task: Task | null; + fetching: boolean; deleteActivity: boolean | null; installedGit: boolean; } @@ -29,12 +30,7 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps { const id = +own.match.params.id; const filtered = state.tasks.current.filter((task) => task.instance.id === id); - let task = null; - if (filtered.length) { - task = filtered[0]; - } else if (state.notifications.errors.tasks.fetching) { - task = undefined; - } + const task = filtered[0] || null; let deleteActivity = null; if (task && id in deletes.byTask) { @@ -44,6 +40,7 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps { return { task, deleteActivity, + fetching: state.tasks.fetching, installedGit: plugins.GIT_INTEGRATION, }; } @@ -69,6 +66,7 @@ function TaskPageContainer(props: StateToProps & DispatchToProps) { return ( dispatch(gettingFormatsAsync()), + loadFormats: (): void => dispatch(getFormatsAsync()), verifyAuthorized: (): void => dispatch(authorizedAsync()), initPlugins: (): void => dispatch(checkPluginsAsync()), loadUsers: (): void => dispatch(getUsersAsync()), @@ -84,10 +90,13 @@ function reduxAppWrapper(props: StateToProps & DispatchToProps) { verifyAuthorized={props.verifyAuthorized} resetErrors={props.resetErrors} resetMessages={props.resetMessages} - pluginsInitialized={props.pluginsInitialized} userInitialized={props.userInitialized} + pluginsInitialized={props.pluginsInitialized} + pluginsFetching={props.pluginsFetching} usersInitialized={props.usersInitialized} + usersFetching={props.usersFetching} formatsInitialized={props.formatsInitialized} + formatsFetching={props.formatsFetching} installedAutoAnnotation={props.installedAutoAnnotation} installedTFSegmentation={props.installedTFSegmentation} installedTFAnnotation={props.installedTFAnnotation} diff --git a/cvat-ui/src/reducers/formats-reducer.ts b/cvat-ui/src/reducers/formats-reducer.ts index ac9d9b62c6f5..8eedb90acb35 100644 --- a/cvat-ui/src/reducers/formats-reducer.ts +++ b/cvat-ui/src/reducers/formats-reducer.ts @@ -7,21 +7,31 @@ const defaultState: FormatsState = { annotationFormats: [], datasetFormats: [], initialized: false, + fetching: false, }; export default (state = defaultState, action: AnyAction): FormatsState => { switch (action.type) { - case FormatsActionTypes.GETTING_FORMATS_SUCCESS: + case FormatsActionTypes.GET_FORMATS: { + return { + ...state, + fetching: true, + initialized: false, + }; + } + case FormatsActionTypes.GET_FORMATS_SUCCESS: return { ...state, initialized: true, + fetching: false, annotationFormats: action.payload.annotationFormats, datasetFormats: action.payload.datasetFormats, }; - case FormatsActionTypes.GETTING_FORMATS_FAILED: + case FormatsActionTypes.GET_FORMATS_FAILED: return { ...state, initialized: true, + fetching: false, }; default: return state; diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 52d5f469dcf1..0675c482f60a 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -22,6 +22,7 @@ export interface Task { export interface TasksState { initialized: boolean; + fetching: boolean; gettingQuery: TasksQuery; count: number; current: Task[]; @@ -58,6 +59,7 @@ export interface TasksState { export interface FormatsState { annotationFormats: any[]; datasetFormats: any[]; + fetching: boolean; initialized: boolean; } @@ -71,6 +73,7 @@ export enum SupportedPlugins { } export interface PluginsState { + fetching: boolean; initialized: boolean; plugins: { [name in SupportedPlugins]: boolean; @@ -79,6 +82,7 @@ export interface PluginsState { export interface UsersState { users: any[]; + fetching: boolean; initialized: boolean; } @@ -117,6 +121,7 @@ export interface Running { export interface ModelsState { initialized: boolean; + fetching: boolean; creatingStatus: string; models: Model[]; runnings: Running[]; diff --git a/cvat-ui/src/reducers/models-reducer.ts b/cvat-ui/src/reducers/models-reducer.ts index 199325cf5a4a..565dc26b8f39 100644 --- a/cvat-ui/src/reducers/models-reducer.ts +++ b/cvat-ui/src/reducers/models-reducer.ts @@ -5,6 +5,7 @@ import { ModelsState } from './interfaces'; const defaultState: ModelsState = { initialized: false, + fetching: false, creatingStatus: '', models: [], visibleRunWindows: false, @@ -18,6 +19,7 @@ export default function (state = defaultState, action: AnyAction): ModelsState { return { ...state, initialized: false, + fetching: true, }; } case ModelsActionTypes.GET_MODELS_SUCCESS: { @@ -25,12 +27,14 @@ export default function (state = defaultState, action: AnyAction): ModelsState { ...state, models: action.payload.models, initialized: true, + fetching: false, }; } case ModelsActionTypes.GET_MODELS_FAILED: { return { ...state, initialized: true, + fetching: false, }; } case ModelsActionTypes.DELETE_MODEL_SUCCESS: { diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index cca896e8c890..d006d7c89e65 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -198,7 +198,7 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case FormatsActionTypes.GETTING_FORMATS_FAILED: { + case FormatsActionTypes.GET_FORMATS_FAILED: { return { ...state, errors: { diff --git a/cvat-ui/src/reducers/plugins-reducer.ts b/cvat-ui/src/reducers/plugins-reducer.ts index f594023c8131..27913b9629ef 100644 --- a/cvat-ui/src/reducers/plugins-reducer.ts +++ b/cvat-ui/src/reducers/plugins-reducer.ts @@ -8,6 +8,7 @@ import { const defaultState: PluginsState = { + fetching: false, initialized: false, plugins: { GIT_INTEGRATION: false, @@ -19,6 +20,13 @@ const defaultState: PluginsState = { }; export default function (state = defaultState, action: AnyAction): PluginsState { switch (action.type) { + case PluginsActionTypes.CHECK_PLUGINS: { + return { + ...state, + initialized: false, + fetching: true, + }; + } case PluginsActionTypes.CHECKED_ALL_PLUGINS: { const { plugins } = action.payload; @@ -29,6 +37,7 @@ export default function (state = defaultState, action: AnyAction): PluginsState return { ...state, initialized: true, + fetching: false, plugins, }; } diff --git a/cvat-ui/src/reducers/tasks-reducer.ts b/cvat-ui/src/reducers/tasks-reducer.ts index b7a6c22ed6f7..4b1be0648728 100644 --- a/cvat-ui/src/reducers/tasks-reducer.ts +++ b/cvat-ui/src/reducers/tasks-reducer.ts @@ -5,6 +5,7 @@ import { TasksState, Task } from './interfaces'; const defaultState: TasksState = { initialized: false, + fetching: false, count: 0, current: [], gettingQuery: { @@ -48,6 +49,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState }, }, initialized: false, + fetching: true, }; case TasksActionTypes.GET_TASKS_SUCCESS: { const combinedWithPreviews = action.payload.array @@ -59,6 +61,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState return { ...state, initialized: true, + fetching: false, count: action.payload.count, current: combinedWithPreviews, gettingQuery: { ...action.payload.query }, @@ -68,6 +71,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState return { ...state, initialized: true, + fetching: false, count: 0, current: [], gettingQuery: { ...action.payload.query }, diff --git a/cvat-ui/src/reducers/users-reducer.ts b/cvat-ui/src/reducers/users-reducer.ts index f367b56ec65d..f86755a19ea6 100644 --- a/cvat-ui/src/reducers/users-reducer.ts +++ b/cvat-ui/src/reducers/users-reducer.ts @@ -5,22 +5,31 @@ import { UsersActionTypes } from '../actions/users-actions'; const initialState: UsersState = { users: [], + fetching: false, initialized: false, }; export default function (state: UsersState = initialState, action: AnyAction): UsersState { switch (action.type) { + case UsersActionTypes.GET_USERS: { + return { + ...state, + fetching: true, + initialized: false, + }; + } case UsersActionTypes.GET_USERS_SUCCESS: return { ...state, + fetching: false, initialized: true, users: action.payload.users, }; case UsersActionTypes.GET_USERS_FAILED: return { ...state, + fetching: false, initialized: true, - users: [], }; default: return { diff --git a/cvat-ui/src/stylesheet.css b/cvat-ui/src/stylesheet.css index 1efae2742083..1a2aef1828a4 100644 --- a/cvat-ui/src/stylesheet.css +++ b/cvat-ui/src/stylesheet.css @@ -771,7 +771,7 @@ textarea.ant-input.cvat-raw-labels-viewer { margin-top: 10px; } -.cvat-create-model-content > div:nth-child(5) > button { +.cvat-create-model-content > div:nth-child(6) > button { margin-top: 10px; float: right; width: 120px; From 56948deddf6bc18af1cc250946656cee9797f7c3 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 28 Nov 2019 08:47:45 +0300 Subject: [PATCH 11/15] Fixed a name of a format --- cvat-ui/src/actions/tasks-actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 911c8e1d0357..e6cda76fd0d5 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -259,7 +259,7 @@ ThunkAction, {}, {}, AnyAction> { dispatch(exportDataset(task, exporter)); try { - const url = await task.annotations.exportDataset(task.name, exporter); + const url = await task.annotations.exportDataset(exporter.tag); // false positive // eslint-disable-next-line security/detect-non-literal-fs-filename window.open(url, '_blank'); From 3c3f30af32de7ca265c3a74263781806846289e5 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 28 Nov 2019 10:48:33 +0300 Subject: [PATCH 12/15] Light refactoring --- cvat-ui/src/components/actions-menu/actions-menu.tsx | 2 +- cvat-ui/src/components/tasks-page/tasks-page.tsx | 5 ++--- cvat-ui/src/containers/tasks-page/tasks-page.tsx | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index 9b0003569a6f..53ee12e79020 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -94,7 +94,7 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) { })) } - + { props.exporters.map((exporter) => ExportItemComponent({ exporter, diff --git a/cvat-ui/src/components/tasks-page/tasks-page.tsx b/cvat-ui/src/components/tasks-page/tasks-page.tsx index 06f0f651ba9e..2b8da555cc01 100644 --- a/cvat-ui/src/components/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/components/tasks-page/tasks-page.tsx @@ -4,7 +4,6 @@ import { withRouter } from 'react-router-dom'; import { Spin, - Modal, } from 'antd'; import { @@ -16,7 +15,7 @@ import EmptyListComponent from './empty-list'; import TaskListContainer from '../../containers/tasks-page/tasks-list'; interface TasksPageProps { - tasksAreBeingFetched: boolean; + tasksFetching: boolean; gettingQuery: TasksQuery; numberOfTasks: number; numberOfVisibleTasks: number; @@ -133,7 +132,7 @@ class TasksPageComponent extends React.PureComponent ); diff --git a/cvat-ui/src/containers/tasks-page/tasks-page.tsx b/cvat-ui/src/containers/tasks-page/tasks-page.tsx index 5b65812b4058..1420ca58927c 100644 --- a/cvat-ui/src/containers/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/containers/tasks-page/tasks-page.tsx @@ -11,7 +11,7 @@ import TasksPageComponent from '../../components/tasks-page/tasks-page'; import { getTasksAsync } from '../../actions/tasks-actions'; interface StateToProps { - tasksAreBeingFetched: boolean; + tasksFetching: boolean; gettingQuery: TasksQuery; numberOfTasks: number; numberOfVisibleTasks: number; @@ -25,7 +25,7 @@ function mapStateToProps(state: CombinedState): StateToProps { const { tasks } = state; return { - tasksAreBeingFetched: !state.tasks.initialized, + tasksFetching: state.tasks.fetching, gettingQuery: tasks.gettingQuery, numberOfTasks: state.tasks.count, numberOfVisibleTasks: state.tasks.current.length, @@ -43,7 +43,7 @@ type TasksPageContainerProps = StateToProps & DispatchToProps; function TasksPageContainer(props: TasksPageContainerProps) { return ( Date: Thu, 28 Nov 2019 14:09:15 +0300 Subject: [PATCH 13/15] Show inference progress --- cvat-ui/src/actions/models-actions.ts | 202 +++++++++++++++++- cvat-ui/src/actions/tasks-actions.ts | 8 + .../components/actions-menu/actions-menu.tsx | 7 +- cvat-ui/src/components/cvat-app.tsx | 10 +- cvat-ui/src/components/feedback.tsx | 2 +- .../src/components/tasks-page/task-item.tsx | 29 ++- .../containers/actions-menu/actions-menu.tsx | 9 +- .../src/containers/tasks-page/task-item.tsx | 4 + cvat-ui/src/reducers/interfaces.ts | 27 ++- cvat-ui/src/reducers/models-reducer.ts | 24 ++- cvat-ui/src/reducers/notifications-reducer.ts | 25 ++- 11 files changed, 327 insertions(+), 20 deletions(-) diff --git a/cvat-ui/src/actions/models-actions.ts b/cvat-ui/src/actions/models-actions.ts index d11549d89e7a..f2a53e30589d 100644 --- a/cvat-ui/src/actions/models-actions.ts +++ b/cvat-ui/src/actions/models-actions.ts @@ -3,7 +3,12 @@ import { ThunkAction } from 'redux-thunk'; import getCore from '../core'; import { getCVATStore } from '../store'; -import { Model, ModelFiles, CombinedState } from '../reducers/interfaces'; +import { + Model, + ModelFiles, + ActiveInference, + CombinedState, +} from '../reducers/interfaces'; export enum ModelsActionTypes { GET_MODELS = 'GET_MODELS', @@ -324,6 +329,199 @@ ThunkAction, {}, {}, AnyAction> { }; } + +function getInferenceStatusSuccess( + taskID: number, + activeInference: ActiveInference, +): AnyAction { + const action = { + type: ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS, + payload: { + taskID, + activeInference, + }, + }; + + return action; +} + +function getInferenceStatusFailed(taskID: number, error: any): AnyAction { + const action = { + type: ModelsActionTypes.GET_INFERENCE_STATUS_FAILED, + payload: { + taskID, + error, + }, + }; + + return action; +} + +interface InferenceMeta { + active: boolean; + taskID: number; + requestID: string; +} + +const timers: any = {}; + +async function timeoutCallback( + url: string, + taskID: number, + dispatch: ActionCreator, +): Promise { + try { + delete timers[taskID]; + + const response = await core.server.request(url, { + method: 'GET', + }); + + const activeInference: ActiveInference = { + status: response.status, + progress: +response.progress || 0, + error: response.error || response.stderr || '', + }; + + + if (activeInference.status === 'unknown') { + dispatch(getInferenceStatusFailed( + taskID, + new Error( + `Inference status for the task ${taskID} is unknown.`, + ), + )); + + return; + } + + if (activeInference.status === 'failed') { + dispatch(getInferenceStatusFailed( + taskID, + new Error( + `Inference status for the task ${taskID} is failed. ${activeInference.error}`, + ), + )); + + return; + } + + if (activeInference.status !== 'finished') { + timers[taskID] = setTimeout( + timeoutCallback.bind( + null, + url, + taskID, + dispatch, + ), 3000, + ); + } + + dispatch(getInferenceStatusSuccess(taskID, activeInference)); + } catch (error) { + dispatch(getInferenceStatusFailed(taskID, error)); + } +} + +function subscribe( + urlPath: string, + inferenceMeta: InferenceMeta, + dispatch: ActionCreator, +): void { + if (!(inferenceMeta.taskID in timers)) { + const requestURL = `${baseURL}/${urlPath}/${inferenceMeta.requestID}`; + timers[inferenceMeta.taskID] = setTimeout( + timeoutCallback.bind( + null, + requestURL, + inferenceMeta.taskID, + dispatch, + ), + ); + } +} + +export function getInferenceStatusAsync(tasks: number[]): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + function parse(response: any): InferenceMeta[] { + return Object.keys(response).map((key: string): InferenceMeta => ({ + taskID: +key, + requestID: response[key].rq_id || key, + active: typeof (response[key].active) === 'undefined' ? ['queued', 'started'] + .includes(response[key].status.toLowerCase()) : response[key].active, + })); + } + + const store = getCVATStore(); + const state: CombinedState = store.getState(); + const OpenVINO = state.plugins.plugins.AUTO_ANNOTATION; + const RCNN = state.plugins.plugins.TF_ANNOTATION; + const MaskRCNN = state.plugins.plugins.TF_SEGMENTATION; + + try { + if (OpenVINO) { + const response = await core.server.request( + `${baseURL}/auto_annotation/meta/get`, { + method: 'POST', + data: JSON.stringify(tasks), + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + parse(response.run) + .filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active) + .forEach((inferenceMeta: InferenceMeta): void => { + subscribe('auto_annotation/check', inferenceMeta, dispatch); + }); + } + + if (RCNN) { + const response = await core.server.request( + `${baseURL}/tensorflow/annotation/meta/get`, { + method: 'POST', + data: JSON.stringify(tasks), + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + parse(response) + .filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active) + .forEach((inferenceMeta: InferenceMeta): void => { + subscribe('tensorflow/annotation/check/task', inferenceMeta, dispatch); + }); + } + + if (MaskRCNN) { + const response = await core.server.request( + `${baseURL}/tensorflow/annotation/meta/get`, { + method: 'POST', + data: JSON.stringify(tasks), + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + parse(response) + .filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active) + .forEach((inferenceMeta: InferenceMeta): void => { + subscribe('tensorflow/segmentation/check/task', inferenceMeta, dispatch); + }); + } + } catch (error) { + tasks.forEach((task: number): void => { + dispatch(getInferenceStatusFailed(task, error)); + }); + } + }; +} + + function inferModel(): AnyAction { const action = { type: ModelsActionTypes.INFER_MODEL, @@ -387,6 +585,8 @@ export function inferModelAsync( }, ); } + + dispatch(getInferenceStatusAsync([taskInstance.id])); } catch (error) { dispatch(inferModelFailed(error)); return; diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index e6cda76fd0d5..8bb5c381708f 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -1,6 +1,7 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; import { TasksQuery } from '../reducers/interfaces'; +import { getInferenceStatusAsync } from './models-actions'; import getCore from '../core'; @@ -93,6 +94,13 @@ ThunkAction, {}, {}, AnyAction> { const previews = []; const promises = array .map((task): string => (task as any).frames.preview()); + dispatch( + getInferenceStatusAsync( + array.map( + (task: any): number => task.id, + ), + ), + ); for (const promise of promises) { try { diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index 53ee12e79020..f6057cb8574b 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -11,7 +11,6 @@ import LoaderItemComponent from './loader-item'; import DumperItemComponent from './dumper-item'; import ExportItemComponent from './export-item'; - interface ActionsMenuComponentProps { taskInstance: any; loaders: any[]; @@ -23,6 +22,7 @@ interface ActionsMenuComponentProps { installedTFAnnotation: boolean; installedTFSegmentation: boolean; installedAutoAnnotation: boolean; + inferenceIsActive: boolean; onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void; onDumpAnnotation: (taskInstance: any, dumper: any) => void; onExportDataset: (taskInstance: any, exporter: any) => void; @@ -106,7 +106,10 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) { } {tracker && Open bug tracker} - {renderModelRunner && Automatic annotation} + { + renderModelRunner && + Automatic annotation + }
Delete
diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 275eb69df515..ba9c40d7c494 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -62,10 +62,14 @@ export default class CVATApplication extends React.PureComponent { } const { tasks } = this.props.notifications.messages; - let shown = !!tasks.loading; + const { models } = this.props.notifications.messages; + let shown = !!tasks.loadingDone || !!models.inferenceDone; - if (tasks.loading) { - showMessage(tasks.loading); + if (tasks.loadingDone) { + showMessage(tasks.loadingDone); + } + if (models.inferenceDone) { + showMessage(models.inferenceDone); } if (shown) { diff --git a/cvat-ui/src/components/feedback.tsx b/cvat-ui/src/components/feedback.tsx index bd00ea8155cc..e4bbf7e84af3 100644 --- a/cvat-ui/src/components/feedback.tsx +++ b/cvat-ui/src/components/feedback.tsx @@ -98,7 +98,7 @@ export default class Feedback extends React.PureComponent<{}, State> { Help to make CVAT better + Help to make CVAT better } content={this.renderContent()} visible={this.state.active} diff --git a/cvat-ui/src/components/tasks-page/task-item.tsx b/cvat-ui/src/components/tasks-page/task-item.tsx index 658a2a700a86..02dd8ba5a219 100644 --- a/cvat-ui/src/components/tasks-page/task-item.tsx +++ b/cvat-ui/src/components/tasks-page/task-item.tsx @@ -15,11 +15,13 @@ import { import moment from 'moment'; import ActionsMenuContainer from '../../containers/actions-menu/actions-menu'; +import { ActiveInference } from '../../reducers/interfaces'; export interface TaskItemProps { taskInstance: any; previewImage: string; deleted: boolean; + activeInference: ActiveInference | null; } class TaskItemComponent extends React.PureComponent { @@ -94,7 +96,8 @@ class TaskItemComponent extends React.PureComponent
- + + + { this.props.activeInference ? + <> + + + Automatic annotation + + + + + + + + : null + } ) } diff --git a/cvat-ui/src/containers/actions-menu/actions-menu.tsx b/cvat-ui/src/containers/actions-menu/actions-menu.tsx index a4eebbe7998b..1fab00afa0f3 100644 --- a/cvat-ui/src/containers/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/containers/actions-menu/actions-menu.tsx @@ -2,7 +2,11 @@ import React from 'react'; import { connect } from 'react-redux'; import ActionsMenuComponent from '../../components/actions-menu/actions-menu'; -import { CombinedState } from '../../reducers/interfaces'; +import { + CombinedState, + ActiveInference, +} from '../../reducers/interfaces'; + import { showRunModelDialog } from '../../actions/models-actions'; import { dumpAnnotationsAsync, @@ -25,6 +29,7 @@ interface StateToProps { installedTFAnnotation: boolean; installedTFSegmentation: boolean; installedAutoAnnotation: boolean; + inferenceIsActive: boolean; }; interface DispatchToProps { @@ -56,6 +61,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { dumpers: formats.annotationFormats .map((format: any): any[] => format.dumpers).flat(), exporters: formats.datasetFormats, + inferenceIsActive: id in state.models.inferences, }; } @@ -97,6 +103,7 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps) onExportDataset={props.onExportDataset} onDeleteTask={props.onDeleteTask} onOpenRunWindow={props.onOpenRunWindow} + inferenceIsActive={props.inferenceIsActive} /> ); } diff --git a/cvat-ui/src/containers/tasks-page/task-item.tsx b/cvat-ui/src/containers/tasks-page/task-item.tsx index b01159750dbf..e13390d7e3f5 100644 --- a/cvat-ui/src/containers/tasks-page/task-item.tsx +++ b/cvat-ui/src/containers/tasks-page/task-item.tsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { TasksQuery, CombinedState, + ActiveInference, } from '../../reducers/interfaces'; import TaskItemComponent from '../../components/tasks-page/task-item' @@ -16,6 +17,7 @@ interface StateToProps { deleteActivity: boolean | null; previewImage: string; taskInstance: any; + activeInference: ActiveInference | null; } interface DispatchToProps { @@ -36,6 +38,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { deleteActivity: deletes.byTask[id] ? deletes.byTask[id] : null, previewImage: task.preview, taskInstance: task.instance, + activeInference: state.models.inferences[id] || null, }; } @@ -55,6 +58,7 @@ function TaskItemContainer(props: TasksItemContainerProps) { deleted={props.deleteActivity === true} taskInstance={props.taskInstance} previewImage={props.previewImage} + activeInference={props.activeInference} /> ); } diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 0675c482f60a..4f107fee8f1f 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -111,12 +111,18 @@ export interface Model { labels: string[]; } -export interface Running { - [tid: string]: { - status: string; - processId: string; - error: any; - }; +export enum RQStatus { + unknown = 'unknown', + queued = 'queued', + started = 'started', + finished = 'finished', + failed = 'failed', +} + +export interface ActiveInference { + status: RQStatus; + progress: number; + error: string; } export interface ModelsState { @@ -124,7 +130,9 @@ export interface ModelsState { fetching: boolean; creatingStatus: string; models: Model[]; - runnings: Running[]; + inferences: { + [index: number]: ActiveInference; + }; visibleRunWindows: boolean; activeRunTask: any; } @@ -173,7 +181,10 @@ export interface NotificationsState { }; messages: { tasks: { - loading: string; + loadingDone: string; + }; + models: { + inferenceDone: string; }; }; } diff --git a/cvat-ui/src/reducers/models-reducer.ts b/cvat-ui/src/reducers/models-reducer.ts index 565dc26b8f39..8372c01f9643 100644 --- a/cvat-ui/src/reducers/models-reducer.ts +++ b/cvat-ui/src/reducers/models-reducer.ts @@ -10,7 +10,7 @@ const defaultState: ModelsState = { models: [], visibleRunWindows: false, activeRunTask: null, - runnings: [], + inferences: {}, }; export default function (state = defaultState, action: AnyAction): ModelsState { @@ -84,6 +84,28 @@ export default function (state = defaultState, action: AnyAction): ModelsState { activeRunTask: null, }; } + case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: { + const inferences = { ...state.inferences }; + if (action.payload.activeInference.status === 'finished') { + delete inferences[action.payload.taskID]; + } else { + inferences[action.payload.taskID] = action.payload.activeInference; + } + + return { + ...state, + inferences, + }; + } + case ModelsActionTypes.GET_INFERENCE_STATUS_FAILED: { + const inferences = { ...state.inferences }; + delete inferences[action.payload.taskID]; + + return { + ...state, + inferences, + }; + } default: { return { ...state, diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index d006d7c89e65..c4291e71cee7 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -46,7 +46,10 @@ const defaultState: NotificationsState = { }, messages: { tasks: { - loading: '', + loadingDone: '', + }, + models: { + inferenceDone: '', }, }, }; @@ -145,7 +148,7 @@ export default function (state = defaultState, action: AnyAction): Notifications ...state.messages, tasks: { ...state.messages.tasks, - loading: `Annotations have been loaded to the task ${task.id}`, + loadingDone: `Annotations have been loaded to the task ${task.id}`, }, }, }; @@ -258,6 +261,24 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: { + if (action.payload.activeInference.status === 'finished') { + return { + ...state, + messages: { + ...state.messages, + models: { + ...state.messages.models, + inferenceDone: `Automatic annotation finished for the task ${action.payload.taskID}`, + }, + }, + }; + } + + return { + ...state, + }; + } case ModelsActionTypes.GET_INFERENCE_STATUS_FAILED: { return { ...state, From 0caf044aea176b132f69f4b0502a2a44e3af9c7d Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 28 Nov 2019 14:29:56 +0300 Subject: [PATCH 14/15] Fixed link --- cvat-ui/src/actions/models-actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-ui/src/actions/models-actions.ts b/cvat-ui/src/actions/models-actions.ts index f2a53e30589d..c2745195f42d 100644 --- a/cvat-ui/src/actions/models-actions.ts +++ b/cvat-ui/src/actions/models-actions.ts @@ -498,7 +498,7 @@ ThunkAction, {}, {}, AnyAction> { if (MaskRCNN) { const response = await core.server.request( - `${baseURL}/tensorflow/annotation/meta/get`, { + `${baseURL}/tensorflow/segmentation/meta/get`, { method: 'POST', data: JSON.stringify(tasks), headers: { From a0a7be12a284d2cd4b79728ce595c80212078517 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 28 Nov 2019 14:44:39 +0300 Subject: [PATCH 15/15] Fixed model loading after a model was uploaded --- .../model-runner-modal/model-runner-modal.tsx | 11 +++++------ .../model-runner-dialog/model-runner-dialog.tsx | 3 +++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx index 7898b452d121..5b347713af42 100644 --- a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx +++ b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx @@ -19,6 +19,7 @@ interface StringObject { } interface Props { + modelsFetching: boolean; modelsInitialized: boolean; models: Model[]; activeProcesses: StringObject; @@ -273,6 +274,10 @@ export default class ModelRunnerModalComponent extends React.PureComponent model.name === this.state.selectedModel diff --git a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx index 233f1b4ae72c..290c4df86e36 100644 --- a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx +++ b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx @@ -14,6 +14,7 @@ import { interface StateToProps { + modelsFetching: boolean; modelsInitialized: boolean; models: Model[]; activeProcesses: { @@ -40,6 +41,7 @@ function mapStateToProps(state: CombinedState): StateToProps { const { models } = state; return { + modelsFetching: models.fetching, modelsInitialized: models.initialized, models: models.models, activeProcesses: {}, @@ -72,6 +74,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { function ModelRunnerModalContainer(props: StateToProps & DispatchToProps) { return (