diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2c6e88d..ea7c4a63 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 ``` diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 4faf9e21..fcd5a2a0 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( @@ -246,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 806e73aa..a3e74685 100644 --- a/cvat-ui/src/actions/formats-actions.ts +++ b/cvat-ui/src/actions/formats-actions.ts @@ -6,38 +6,53 @@ 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(formats: any): AnyAction { +function getFormats(): AnyAction { return { - type: FormatsActionTypes.GETTING_FORMATS_SUCCESS, + type: FormatsActionTypes.GET_FORMATS, + payload: {}, + }; +} + +function getFormatsSuccess( + annotationFormats: any[], + datasetFormats: any[], +): AnyAction { + return { + type: FormatsActionTypes.GET_FORMATS_SUCCESS, payload: { - formats, + annotationFormats, + datasetFormats, }, }; } -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 => { - let formats = null; + dispatch(getFormats()); + 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)); + dispatch(getFormatsFailed(error)); return; } - dispatch(gettingFormatsSuccess(formats)); + dispatch(getFormatsSuccess(annotationFormats, datasetFormats)); }; } diff --git a/cvat-ui/src/actions/models-actions.ts b/cvat-ui/src/actions/models-actions.ts index d11549d8..c2745195 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/segmentation/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/notification-actions.ts b/cvat-ui/src/actions/notification-actions.ts new file mode 100644 index 00000000..b8431106 --- /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/plugins-actions.ts b/cvat-ui/src/actions/plugins-actions.ts index 89c04d03..0e27013f 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/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index b5437823..8bb5c381 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'; @@ -16,6 +17,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 +30,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 { @@ -89,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 { @@ -150,7 +162,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 +224,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(exporter.tag); + // 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 c64cf111..3d75197a 100644 --- a/cvat-ui/src/actions/users-actions.ts +++ b/cvat-ui/src/actions/users-actions.ts @@ -14,7 +14,7 @@ export enum UsersActionTypes { function getUsers(): AnyAction { const action = { type: UsersActionTypes.GET_USERS, - payload: { }, + payload: {}, }; return action; @@ -41,8 +41,9 @@ function getUsersFailed(error: any): AnyAction { export function getUsersAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { + dispatch(getUsers()); + 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 c782aaf2..f6057cb8 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; + inferenceIsActive: 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,8 +94,22 @@ 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} + { + renderModelRunner && + Automatic annotation + }
Delete
diff --git a/cvat-ui/src/components/actions-menu/dumper-item.tsx b/cvat-ui/src/components/actions-menu/dumper-item.tsx index e9e9c36e..ca862a8e 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 00000000..157fd495 --- /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-model-page/create-model-content.tsx b/cvat-ui/src/components/create-model-page/create-model-content.tsx index 94c36e11..92719519 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,8 +9,11 @@ import { Tooltip, Modal, message, + notification, } from 'antd'; +import Text from 'antd/lib/typography/Text'; + import CreateModelForm, { CreateModelForm as WrappedCreateModelForm } from './create-model-form'; @@ -22,7 +25,6 @@ import { ModelFiles } from '../../reducers/interfaces'; interface Props { createModel(name: string, files: ModelFiles, global: boolean): void; isAdmin: boolean; - modelCreatingError: string; modelCreatingStatus: string; } @@ -64,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', }); }) } @@ -86,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() { @@ -106,7 +101,9 @@ export default class CreateModelContent extends React.PureComponent { - {window.open(guideLink, '_blank')}} type='question-circle'/> + { + window.open(guideLink, '_blank') + }} type='question-circle'/> @@ -116,11 +113,15 @@ export default class CreateModelContent extends React.PureComponent { } /> + + * + Select files: + this.fileManagerContainer = container - }/> + } withRemote={true}/> {status && } 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 44eb7b23..1a196593 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,14 +43,13 @@ export class CreateModelForm extends React.PureComponent { return (
e.preventDefault()}> - - - Name - - + + * + Name: + - + { getFieldDecorator('name', { rules: [{ required: true, @@ -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 1f1e2e05..e58a8ff1 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; } @@ -21,10 +20,9 @@ export default function CreateModelPageComponent(props: Props) { return ( - {`Upload a new model`} + Upload a new model 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 93c100ca..56cc88da 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 2d0d5d0a..e0b877a4 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 9b4ac355..57801a1b 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,10 +4,9 @@ import { Row, Col, Alert, - Modal, Button, Collapse, - message, + notification, } from 'antd'; import Text from 'antd/lib/typography/Text'; @@ -28,7 +27,6 @@ export interface CreateTaskData { interface Props { onCreate: (data: CreateTaskData) => void; status: string; - error: string; installedGit: boolean; } @@ -90,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; } @@ -117,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', }); }); } @@ -137,7 +135,8 @@ export default class CreateTaskContent extends React.PureComponent private renderLabelsBlock() { return ( - Labels + * + Labels: private renderFilesBlock() { return ( + * + Select files: this.fileManagerContainer = container @@ -169,7 +170,7 @@ export default class CreateTaskContent extends React.PureComponent {'Advanced configuration'} + Advanced configuration } key='1'> } 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) { @@ -213,12 +209,12 @@ 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 ( - {'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 d6155843..2287ab5c 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; } @@ -20,10 +19,9 @@ export default function CreateTaskPage(props: Props) { return ( - {'Create a new task'} + Create a new task diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 6d7ad26f..ba9c40d7 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -1,12 +1,17 @@ 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, + notification, +} from 'antd'; import TasksPageContainer from '../containers/tasks-page/tasks-page'; import CreateTaskPageContainer from '../containers/create-task-page/create-task-page'; @@ -20,22 +25,26 @@ 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; - pluginsInitialized: boolean; + resetErrors: () => void; + resetMessages: () => void; userInitialized: boolean; + pluginsInitialized: boolean; + pluginsFetching: boolean; formatsInitialized: boolean; + formatsFetching: boolean; usersInitialized: boolean; - gettingAuthError: string; - gettingFormatsError: string; - gettingUsersError: string; + usersFetching: boolean; installedAutoAnnotation: boolean; installedTFAnnotation: boolean; installedTFSegmentation: boolean; + notifications: NotificationsState; user: any; } @@ -44,54 +53,141 @@ export default class CVATApplication extends React.PureComponent { super(props); } - public componentDidMount() { - this.props.verifyAuthorized(); - } + private showMessages() { + function showMessage(title: string) { + notification.info({ + message: title, + duration: null, + }); + } - public componentDidUpdate() { - if (!this.props.userInitialized || - this.props.userInitialized && this.props.user == null) { - return; + const { tasks } = this.props.notifications.messages; + const { models } = this.props.notifications.messages; + let shown = !!tasks.loadingDone || !!models.inferenceDone; + + if (tasks.loadingDone) { + showMessage(tasks.loadingDone); } + if (models.inferenceDone) { + showMessage(models.inferenceDone); + } + + if (shown) { + this.props.resetMessages(); + } + } - if (this.props.gettingAuthError) { - Modal.error({ - title: 'Could not check authorization', - content: `${this.props.gettingAuthError}`, + private showErrors() { + function showError(title: string, _error: any) { + const error = _error.toString(); + notification.error({ + message: title, + duration: null, + description: error.length > 200 ? '' : error, }); - return; + + console.error(error); } - if (!this.props.formatsInitialized) { - this.props.loadFormats(); - return; + 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 (this.props.gettingFormatsError) { - Modal.error({ - title: 'Could not receive annotations formats', - content: `${this.props.gettingFormatsError}`, - }); - return; + if (shown) { + this.props.resetErrors(); } + } - if (!this.props.usersInitialized) { - this.props.loadUsers(); + public componentDidMount() { + this.props.verifyAuthorized(); + } + + public componentDidUpdate() { + this.showErrors(); + this.showMessages(); + + if (!this.props.userInitialized || this.props.user == null) { + // not authorized user return; } - if (this.props.gettingUsersError) { - Modal.error({ - title: 'Could not receive users', - content: `${this.props.gettingUsersError}`, - }); + if (!this.props.formatsInitialized && !this.props.formatsFetching) { + this.props.loadFormats(); + } - return; + if (!this.props.usersInitialized && !this.props.usersFetching) { + this.props.loadUsers(); } - if (!this.props.pluginsInitialized) { + if (!this.props.pluginsInitialized && !this.props.pluginsFetching) { this.props.initPlugins(); - return; } } diff --git a/cvat-ui/src/components/feedback.tsx b/cvat-ui/src/components/feedback.tsx index bd00ea81..e4bbf7e8 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/file-manager/file-manager.tsx b/cvat-ui/src/components/file-manager/file-manager.tsx index 6f27f2ca..e34aeaba 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 + } ); @@ -184,13 +184,12 @@ export default class FileManager extends React.PureComponent { public render() { return ( <> - {'Select files'} this.setState({ active: activeKey as any, })}> { this.renderLocalSelector() } { this.renderShareSelector() } - { this.props.withRemote ? this.renderRemoteSelector() : null } + { this.props.withRemote && this.renderRemoteSelector() } ); diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 5f04b5ab..0efeb86b 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/labels-editor/labels-editor.tsx b/cvat-ui/src/components/labels-editor/labels-editor.tsx index 6c7f24a1..f49b2e22 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/login-page/login-page.tsx b/cvat-ui/src/components/login-page/login-page.tsx index 96d76da3..fb2befa0 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 aeb04393..5b347713 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,23 +14,23 @@ import { import { Model } from '../../reducers/interfaces'; +interface StringObject { + [index: string]: string; +} + interface Props { + modelsFetching: boolean; modelsInitialized: boolean; models: Model[]; - activeProcesses: { - [index: string]: string; - }; + activeProcesses: StringObject; visible: boolean; taskInstance: any; - startingError: string; getModels(): void; closeDialog(): void; 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,11 @@ 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) + } - public componentDidMount() { - if (!this.props.modelsInitialized) { - this.props.getModels(); + return acc; + }, {}); + + this.setState({ + mapping: defaultMapping, + }); + } } } diff --git a/cvat-ui/src/components/models-page/models-page.tsx b/cvat-ui/src/components/models-page/models-page.tsx index f6d3df50..8db88abe 100644 --- a/cvat-ui/src/components/models-page/models-page.tsx +++ b/cvat-ui/src/components/models-page/models-page.tsx @@ -14,8 +14,8 @@ interface Props { installedAutoAnnotation: boolean; installedTFSegmentation: boolean; installedTFAnnotation: boolean; - modelsAreBeingFetched: boolean; - modelsFetchingError: any; + modelsInitialized: boolean; + modelsFetching: boolean; registeredUsers: any[]; models: Model[]; getModels(): void; @@ -23,7 +23,7 @@ interface Props { } export default function ModelsPageComponent(props: Props) { - if (props.modelsAreBeingFetched) { + if (!props.modelsInitialized && !props.modelsFetching) { props.getModels(); return ( @@ -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/components/models-page/uploaded-model-item.tsx b/cvat-ui/src/components/models-page/uploaded-model-item.tsx index 01d48897..bad3f1a6 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 diff --git a/cvat-ui/src/components/register-page/register-page.tsx b/cvat-ui/src/components/register-page/register-page.tsx index 5605789e..d38c0234 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/details.tsx b/cvat-ui/src/components/task-page/details.tsx index 11cccc2b..355f2e27 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'; @@ -209,12 +210,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, @@ -280,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({ @@ -296,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 ada1fa5c..6e29066e 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -6,7 +6,7 @@ import { Col, Row, Spin, - Modal, + notification, } from 'antd'; import TopBarComponent from './top-bar'; @@ -15,10 +15,8 @@ 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 | null; + fetching: boolean; deleteActivity: boolean | null; installedGit: boolean; onFetchTask: (tid: number) => void; @@ -27,55 +25,48 @@ 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'); } - 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, + 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 && !this.props.taskFetchingError; + 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 ( ); - } 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/task-item.tsx b/cvat-ui/src/components/tasks-page/task-item.tsx index 658a2a70..02dd8ba5 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/components/tasks-page/tasks-page.tsx b/cvat-ui/src/components/tasks-page/tasks-page.tsx index 424620ca..2b8da555 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,12 +15,7 @@ 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; + tasksFetching: boolean; gettingQuery: TasksQuery; numberOfTasks: number; numberOfVisibleTasks: number; @@ -137,45 +131,8 @@ class TasksPageComponent extends React.PureComponent ); diff --git a/cvat-ui/src/containers/actions-menu/actions-menu.tsx b/cvat-ui/src/containers/actions-menu/actions-menu.tsx index 066dc364..1fab00af 100644 --- a/cvat-ui/src/containers/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/containers/actions-menu/actions-menu.tsx @@ -2,11 +2,16 @@ 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, loadAnnotationsAsync, + exportDatasetAsync, deleteTaskAsync, } from '../../actions/tasks-actions'; @@ -17,24 +22,30 @@ interface OwnProps { interface StateToProps { loaders: any[]; dumpers: any[]; + exporters: any[]; loadActivity: string | null; dumpActivities: string[] | null; + exportActivities: string[] | null; installedTFAnnotation: boolean; installedTFSegmentation: boolean; installedAutoAnnotation: boolean; + inferenceIsActive: boolean; }; interface DispatchToProps { 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; } 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 +54,15 @@ 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, + inferenceIsActive: id in state.models.inferences, + }; } function mapDispatchToProps(dispatch: any): DispatchToProps { @@ -57,6 +73,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,15 +91,19 @@ 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} + inferenceIsActive={props.inferenceIsActive} /> ); } 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 1e47db0d..188589f4 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/create-task-page/create-task-page.tsx b/cvat-ui/src/containers/create-task-page/create-task-page.tsx index 2deb9461..a7655dce 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 c1c6e13a..76974d2a 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 8fa8ef0d..290c4df8 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,7 @@ import { interface StateToProps { - startingError: any; + modelsFetching: boolean; modelsInitialized: boolean; models: Model[]; activeProcesses: { @@ -41,12 +41,12 @@ function mapStateToProps(state: CombinedState): StateToProps { const { models } = state; return { + modelsFetching: models.fetching, modelsInitialized: models.initialized, models: models.models, activeProcesses: {}, taskInstance: models.activeRunTask, visible: models.visibleRunWindows, - startingError: models.startingError, }; } @@ -74,6 +74,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { function ModelRunnerModalContainer(props: StateToProps & DispatchToProps) { return ( ); } diff --git a/cvat-ui/src/containers/models-page/models-page.tsx b/cvat-ui/src/containers/models-page/models-page.tsx index e10cff23..76c2e7f8 100644 --- a/cvat-ui/src/containers/models-page/models-page.tsx +++ b/cvat-ui/src/containers/models-page/models-page.tsx @@ -15,8 +15,8 @@ interface StateToProps { installedAutoAnnotation: boolean; installedTFAnnotation: boolean; installedTFSegmentation: boolean; - modelsAreBeingFetched: boolean; - modelsFetchingError: any; + modelsInitialized: boolean; + modelsFetching: boolean; models: Model[]; registeredUsers: any[]; } @@ -34,8 +34,8 @@ function mapStateToProps(state: CombinedState): StateToProps { installedAutoAnnotation: plugins.AUTO_ANNOTATION, installedTFAnnotation: plugins.TF_ANNOTATION, installedTFSegmentation: plugins.TF_SEGMENTATION, - modelsAreBeingFetched: !models.initialized, - modelsFetchingError: models.fetchingError, + modelsInitialized: models.initialized, + modelsFetching: models.fetching, models: models.models, registeredUsers: state.users.users, }; @@ -63,8 +63,8 @@ function ModelsPageContainer(props: DispatchToProps & StateToProps) { installedAutoAnnotation={props.installedAutoAnnotation} installedTFSegmentation={props.installedTFSegmentation} installedTFAnnotation={props.installedTFAnnotation} - modelsAreBeingFetched={props.modelsAreBeingFetched} - modelsFetchingError={props.modelsFetchingError} + modelsInitialized={props.modelsInitialized} + modelsFetching={props.modelsFetching} 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 bdd7f230..b4e8a305 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 bc29944f..844df084 100644 --- a/cvat-ui/src/containers/task-page/task-page.tsx +++ b/cvat-ui/src/containers/task-page/task-page.tsx @@ -14,10 +14,8 @@ import { type Props = RouteComponentProps<{id: string}>; interface StateToProps { - task: Task; - taskFetchingError: any; - taskUpdatingError: any; - taskDeletingError: any; + task: Task | null; + fetching: boolean; deleteActivity: boolean | null; installedGit: boolean; } @@ -29,7 +27,6 @@ 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); @@ -42,10 +39,8 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps { return { task, - taskFetchingError: state.tasks.tasksFetchingError, - taskUpdatingError: state.tasks.taskUpdatingError, - taskDeletingError, deleteActivity, + fetching: state.tasks.fetching, installedGit: plugins.GIT_INTEGRATION, }; } @@ -71,9 +66,7 @@ function TaskPageContainer(props: StateToProps & DispatchToProps) { return ( ); } diff --git a/cvat-ui/src/containers/tasks-page/tasks-page.tsx b/cvat-ui/src/containers/tasks-page/tasks-page.tsx index 1ad0d54c..1420ca58 100644 --- a/cvat-ui/src/containers/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/containers/tasks-page/tasks-page.tsx @@ -11,12 +11,7 @@ import TasksPageComponent from '../../components/tasks-page/tasks-page'; import { getTasksAsync } from '../../actions/tasks-actions'; interface StateToProps { - deletingError: any; - dumpingError: any; - loadingError: any; - tasksFetchingError: any; - loadingDoneMessage: string; - tasksAreBeingFetched: boolean; + tasksFetching: boolean; gettingQuery: TasksQuery; numberOfTasks: number; numberOfVisibleTasks: number; @@ -28,18 +23,9 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState): StateToProps { const { tasks } = state; - const { activities } = tasks; - const { dumps } = activities; - const { loads } = activities; - const { deletes } = activities; return { - deletingError: deletes.deletingError, - dumpingError: dumps.dumpingError, - loadingError: loads.loadingError, - tasksFetchingError: tasks.tasksFetchingError, - loadingDoneMessage: loads.loadingDoneMessage, - tasksAreBeingFetched: !state.tasks.initialized, + tasksFetching: state.tasks.fetching, gettingQuery: tasks.gettingQuery, numberOfTasks: state.tasks.count, numberOfVisibleTasks: state.tasks.current.length, @@ -57,12 +43,7 @@ type TasksPageContainerProps = StateToProps & DispatchToProps; function TasksPageContainer(props: TasksPageContainerProps) { return ( void; loadUsers: () => void; initPlugins: () => void; + resetErrors: () => void; + resetMessages: () => void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -45,26 +55,29 @@ function mapStateToProps(state: CombinedState): StateToProps { const { users } = state; return { - pluginsInitialized: plugins.initialized, userInitialized: auth.initialized, + pluginsInitialized: plugins.initialized, + pluginsFetching: plugins.fetching, usersInitialized: users.initialized, + usersFetching: users.fetching, formatsInitialized: formats.initialized, - gettingAuthError: auth.authError, - gettingUsersError: users.gettingUsersError, - gettingFormatsError: formats.gettingFormatsError, + formatsFetching: formats.fetching, installedAutoAnnotation: plugins.plugins.AUTO_ANNOTATION, installedTFSegmentation: plugins.plugins.TF_SEGMENTATION, installedTFAnnotation: plugins.plugins.TF_ANNOTATION, + notifications: {...state.notifications}, user: auth.user, }; } function mapDispatchToProps(dispatch: any): DispatchToProps { return { - loadFormats: (): void => dispatch(gettingFormatsAsync()), + loadFormats: (): void => dispatch(getFormatsAsync()), verifyAuthorized: (): void => dispatch(authorizedAsync()), initPlugins: (): void => dispatch(checkPluginsAsync()), loadUsers: (): void => dispatch(getUsersAsync()), + resetErrors: (): void => dispatch(resetErrors()), + resetMessages: (): void => dispatch(resetMessages()), }; } @@ -75,16 +88,19 @@ function reduxAppWrapper(props: StateToProps & DispatchToProps) { loadFormats={props.loadFormats} loadUsers={props.loadUsers} verifyAuthorized={props.verifyAuthorized} - pluginsInitialized={props.pluginsInitialized} + resetErrors={props.resetErrors} + resetMessages={props.resetMessages} userInitialized={props.userInitialized} + pluginsInitialized={props.pluginsInitialized} + pluginsFetching={props.pluginsFetching} usersInitialized={props.usersInitialized} + usersFetching={props.usersFetching} formatsInitialized={props.formatsInitialized} - gettingAuthError={props.gettingAuthError ? props.gettingAuthError.toString() : ''} - gettingFormatsError={props.gettingFormatsError ? props.gettingFormatsError.toString() : ''} - gettingUsersError={props.gettingUsersError ? props.gettingUsersError.toString() : ''} + formatsFetching={props.formatsFetching} 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 d4c9cc97..b4cf915a 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 2bdc2f92..8eedb90a 100644 --- a/cvat-ui/src/reducers/formats-reducer.ts +++ b/cvat-ui/src/reducers/formats-reducer.ts @@ -4,27 +4,34 @@ import { FormatsActionTypes } from '../actions/formats-actions'; import { FormatsState } from './interfaces'; const defaultState: FormatsState = { - loaders: [], - dumpers: [], - gettingFormatsError: null, + 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, - gettingFormatsError: null, - dumpers: action.payload.formats.map((format: any): any[] => format.dumpers).flat(), - loaders: action.payload.formats.map((format: any): any[] => format.loaders).flat(), + fetching: false, + annotationFormats: action.payload.annotationFormats, + datasetFormats: action.payload.datasetFormats, }; - case FormatsActionTypes.GETTING_FORMATS_FAILED: + case FormatsActionTypes.GET_FORMATS_FAILED: return { ...state, initialized: true, - gettingFormatsError: action.payload.error, + fetching: false, }; default: return state; diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index bd51b400..4f107fee 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,45 @@ export interface Task { export interface TasksState { initialized: boolean; - tasksFetchingError: any; - taskUpdatingError: any; + fetching: boolean; 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[]; + fetching: boolean; initialized: boolean; - gettingFormatsError: any; } // eslint-disable-next-line import/prefer-default-export @@ -77,6 +73,7 @@ export enum SupportedPlugins { } export interface PluginsState { + fetching: boolean; initialized: boolean; plugins: { [name in SupportedPlugins]: boolean; @@ -85,8 +82,8 @@ export interface PluginsState { export interface UsersState { users: any[]; + fetching: boolean; initialized: boolean; - gettingUsersError: any; } export interface ShareFileInfo { // get this data from cvat-core @@ -102,7 +99,6 @@ export interface ShareItem { export interface ShareState { root: ShareItem; - error: any; } export interface Model { @@ -115,25 +111,28 @@ 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 { initialized: boolean; + fetching: boolean; creatingStatus: string; - creatingError: any; - startingError: any; - fetchingError: any; - deletingErrors: { // by id - [index: number]: any; - }; models: Model[]; - runnings: Running[]; + inferences: { + [index: number]: ActiveInference; + }; visibleRunWindows: boolean; activeRunTask: any; } @@ -146,6 +145,50 @@ 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: { + loadingDone: string; + }; + models: { + inferenceDone: string; + }; + }; +} + export interface CombinedState { auth: AuthState; tasks: TasksState; @@ -154,4 +197,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 ec241ac5..8372c01f 100644 --- a/cvat-ui/src/reducers/models-reducer.ts +++ b/cvat-ui/src/reducers/models-reducer.ts @@ -5,15 +5,12 @@ import { ModelsState } from './interfaces'; const defaultState: ModelsState = { initialized: false, + fetching: false, creatingStatus: '', - creatingError: null, - startingError: null, - fetchingError: null, - deletingErrors: {}, models: [], visibleRunWindows: false, activeRunTask: null, - runnings: [], + inferences: {}, }; export default function (state = defaultState, action: AnyAction): ModelsState { @@ -21,8 +18,8 @@ export default function (state = defaultState, action: AnyAction): ModelsState { case ModelsActionTypes.GET_MODELS: { return { ...state, - fetchingError: null, initialized: false, + fetching: true, }; } case ModelsActionTypes.GET_MODELS_SUCCESS: { @@ -30,21 +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, - fetchingError: action.payload.error, initialized: true, - }; - } - case ModelsActionTypes.DELETE_MODEL: { - const errors = { ...state.deletingErrors }; - delete errors[action.payload.id]; - return { - ...state, - deletingErrors: errors, + fetching: false, }; } case ModelsActionTypes.DELETE_MODEL_SUCCESS: { @@ -55,18 +45,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 +60,6 @@ export default function (state = defaultState, action: AnyAction): ModelsState { case ModelsActionTypes.CREATE_MODEL_FAILED: { return { ...state, - creatingError: action.payload.error, creatingStatus: '', }; } @@ -90,30 +70,40 @@ export default function (state = defaultState, action: AnyAction): ModelsState { creatingStatus: 'CREATED', }; } - case ModelsActionTypes.INFER_MODEL: { + case ModelsActionTypes.SHOW_RUN_MODEL_DIALOG: { return { ...state, - startingError: null, + visibleRunWindows: true, + activeRunTask: action.payload.taskInstance, }; } - case ModelsActionTypes.INFER_MODEL_FAILED: { + case ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG: { return { ...state, - startingError: action.payload.error, + visibleRunWindows: false, + activeRunTask: null, }; } - case ModelsActionTypes.SHOW_RUN_MODEL_DIALOG: { + 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, - visibleRunWindows: true, - activeRunTask: action.payload.taskInstance, + inferences, }; } - case ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG: { + case ModelsActionTypes.GET_INFERENCE_STATUS_FAILED: { + const inferences = { ...state.inferences }; + delete inferences[action.payload.taskID]; + return { ...state, - visibleRunWindows: false, - activeRunTask: null, + inferences, }; } default: { diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts new file mode 100644 index 00000000..c4291e71 --- /dev/null +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -0,0 +1,340 @@ +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: { + loadingDone: '', + }, + models: { + inferenceDone: '', + }, + }, +}; + +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, + loadingDone: `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.GET_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_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, + 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/plugins-reducer.ts b/cvat-ui/src/reducers/plugins-reducer.ts index f594023c..27913b96 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/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index d6073be0..52d0723b 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 6ce035e1..b85ce562 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 ea27bfa3..4b1be064 100644 --- a/cvat-ui/src/reducers/tasks-reducer.ts +++ b/cvat-ui/src/reducers/tasks-reducer.ts @@ -5,8 +5,7 @@ import { TasksState, Task } from './interfaces'; const defaultState: TasksState = { initialized: false, - tasksFetchingError: null, - taskUpdatingError: null, + fetching: false, count: 0, current: [], gettingQuery: { @@ -21,52 +20,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,11 +45,11 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks activities: { ...state.activities, deletes: { - deletingError: null, byTask: {}, }, }, initialized: false, + fetching: true, }; case TasksActionTypes.GET_TASKS_SUCCESS: { const combinedWithPreviews = action.payload.array @@ -90,6 +61,7 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks return { ...state, initialized: true, + fetching: false, count: action.payload.count, current: combinedWithPreviews, gettingQuery: { ...action.payload.query }, @@ -99,10 +71,10 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks return { ...state, initialized: true, + fetching: false, count: 0, current: [], gettingQuery: { ...action.payload.query }, - tasksFetchingError: action.payload.error, }; case TasksActionTypes.DUMP_ANNOTATIONS: { const { task } = action.payload; @@ -115,8 +87,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 +122,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 +140,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 +241,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 +260,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks ...state.activities, loads: { ...tasksLoadingActivity, - loadingError, }, }, }; @@ -273,7 +302,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 +316,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks ...state.activities, deletes: { ...deletesActivities, - deletingError: error, }, }, }; @@ -299,7 +326,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks activities: { ...state.activities, creates: { - creatingError: null, status: '', }, }, @@ -332,15 +358,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 +372,6 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks case TasksActionTypes.UPDATE_TASK: { return { ...state, - taskUpdatingError: null, }; } case TasksActionTypes.UPDATE_TASK_SUCCESS: { @@ -369,7 +392,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 cb821d1c..f86755a1 100644 --- a/cvat-ui/src/reducers/users-reducer.ts +++ b/cvat-ui/src/reducers/users-reducer.ts @@ -5,30 +5,31 @@ import { UsersActionTypes } from '../actions/users-actions'; const initialState: UsersState = { users: [], + fetching: false, initialized: false, - gettingUsersError: null, }; export default function (state: UsersState = initialState, action: AnyAction): UsersState { switch (action.type) { - case UsersActionTypes.GET_USERS: + case UsersActionTypes.GET_USERS: { return { ...state, + fetching: true, initialized: false, - gettingUsersError: null, }; + } 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: [], - gettingUsersError: action.payload.error, }; default: return { diff --git a/cvat-ui/src/stylesheet.css b/cvat-ui/src/stylesheet.css index c170431d..1a2aef18 100644 --- a/cvat-ui/src/stylesheet.css +++ b/cvat-ui/src/stylesheet.css @@ -316,6 +316,18 @@ background-color: rgba(24,144,255,0.05); } +.cvat-actions-menu-dump-submenu-item > button { + 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); } @@ -759,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;