diff --git a/cvat-ui/src/actions/models-actions.ts b/cvat-ui/src/actions/models-actions.ts index f5330cb6..c91ac39b 100644 --- a/cvat-ui/src/actions/models-actions.ts +++ b/cvat-ui/src/actions/models-actions.ts @@ -2,17 +2,20 @@ // // SPDX-License-Identifier: MIT -import { AnyAction, Dispatch, ActionCreator } from 'redux'; -import { ThunkAction } from 'redux-thunk'; - -import getCore from 'cvat-core'; -import { getCVATStore } from 'cvat-store'; +import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { Model, + ModelType, ModelFiles, ActiveInference, CombinedState, -} from '../reducers/interfaces'; +} from 'reducers/interfaces'; +import getCore from 'cvat-core'; + +export enum PreinstalledModels { + RCNN = 'RCNN Object Detector', + MaskRCNN = 'Mask RCNN Object Detector', +} export enum ModelsActionTypes { GET_MODELS = 'GET_MODELS', @@ -25,66 +28,101 @@ export enum ModelsActionTypes { CREATE_MODEL_SUCCESS = 'CREATE_MODEL_SUCCESS', CREATE_MODEL_FAILED = 'CREATE_MODEL_FAILED', CREATE_MODEL_STATUS_UPDATED = 'CREATE_MODEL_STATUS_UPDATED', - INFER_MODEL = 'INFER_MODEL', - INFER_MODEL_SUCCESS = 'INFER_MODEL_SUCCESS', - INFER_MODEL_FAILED = 'INFER_MODEL_FAILED', - FETCH_META_FAILED = 'FETCH_META_FAILED', - GET_INFERENCE_STATUS = 'GET_INFERENCE_STATUS', + START_INFERENCE_FAILED = 'START_INFERENCE_FAILED', GET_INFERENCE_STATUS_SUCCESS = 'GET_INFERENCE_STATUS_SUCCESS', GET_INFERENCE_STATUS_FAILED = 'GET_INFERENCE_STATUS_FAILED', + FETCH_META_FAILED = 'FETCH_META_FAILED', SHOW_RUN_MODEL_DIALOG = 'SHOW_RUN_MODEL_DIALOG', CLOSE_RUN_MODEL_DIALOG = 'CLOSE_RUN_MODEL_DIALOG', + CANCEL_INFERENCE_SUCCESS = 'CANCEL_INFERENCE_SUCCESS', + CANCEL_INFERENCE_FAILED = 'CANCEL_INFERENCE_FAILED', } -export enum PreinstalledModels { - RCNN = 'RCNN Object Detector', - MaskRCNN = 'Mask RCNN Object Detector', -} - -const core = getCore(); -const baseURL = core.config.backendAPI.slice(0, -7); - -function getModels(): AnyAction { - const action = { - type: ModelsActionTypes.GET_MODELS, - payload: {}, - }; - - return action; -} - -function getModelsSuccess(models: Model[]): AnyAction { - const action = { - type: ModelsActionTypes.GET_MODELS_SUCCESS, - payload: { +export const modelsActions = { + getModels: () => createAction(ModelsActionTypes.GET_MODELS), + getModelsSuccess: (models: Model[]) => createAction( + ModelsActionTypes.GET_MODELS_SUCCESS, { models, }, - }; - - return action; -} - -function getModelsFailed(error: any): AnyAction { - const action = { - type: ModelsActionTypes.GET_MODELS_FAILED, - payload: { + ), + getModelsFailed: (error: any) => createAction( + ModelsActionTypes.GET_MODELS_FAILED, { error, }, - }; + ), + deleteModelSuccess: (id: number) => createAction( + ModelsActionTypes.DELETE_MODEL_SUCCESS, { + id, + }, + ), + deleteModelFailed: (id: number, error: any) => createAction( + ModelsActionTypes.DELETE_MODEL_FAILED, { + error, id, + }, + ), + createModel: () => createAction(ModelsActionTypes.CREATE_MODEL), + createModelSuccess: () => createAction(ModelsActionTypes.CREATE_MODEL_SUCCESS), + createModelFailed: (error: any) => createAction( + ModelsActionTypes.CREATE_MODEL_FAILED, { + error, + }, + ), + createModelUpdateStatus: (status: string) => createAction( + ModelsActionTypes.CREATE_MODEL_STATUS_UPDATED, { + status, + }, + ), + fetchMetaFailed: (error: any) => createAction(ModelsActionTypes.FETCH_META_FAILED, { error }), + getInferenceStatusSuccess: (taskID: number, activeInference: ActiveInference) => createAction( + ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS, { + taskID, + activeInference, + }, + ), + getInferenceStatusFailed: (taskID: number, error: any) => createAction( + ModelsActionTypes.GET_INFERENCE_STATUS_FAILED, { + taskID, + error, + }, + ), + startInferenceFailed: (taskID: number, error: any) => createAction( + ModelsActionTypes.START_INFERENCE_FAILED, { + taskID, + error, + }, + ), + cancelInferenceSuccess: (taskID: number) => createAction( + ModelsActionTypes.CANCEL_INFERENCE_SUCCESS, { + taskID, + }, + ), + cancelInferenceFaild: (taskID: number, error: any) => createAction( + ModelsActionTypes.CANCEL_INFERENCE_FAILED, { + taskID, + error, + }, + ), + closeRunModelDialog: () => createAction(ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG), + showRunModelDialog: (taskInstance: any) => createAction( + ModelsActionTypes.SHOW_RUN_MODEL_DIALOG, { + taskInstance, + }, + ), +}; - return action; -} +export type ModelsActions = ActionUnion; -export function getModelsAsync(): -ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - const store = getCVATStore(); - const state: CombinedState = store.getState(); +const core = getCore(); +const baseURL = core.config.backendAPI.slice(0, -7); + +export function getModelsAsync(): ThunkAction { + return async (dispatch, getState): Promise => { + const state: CombinedState = getState(); const OpenVINO = state.plugins.list.AUTO_ANNOTATION; const RCNN = state.plugins.list.TF_ANNOTATION; const MaskRCNN = state.plugins.list.TF_SEGMENTATION; - dispatch(getModels()); + dispatch(modelsActions.getModels()); const models: Model[] = []; try { @@ -170,108 +208,31 @@ ThunkAction, {}, {}, AnyAction> { }); } } catch (error) { - dispatch(getModelsFailed(error)); + dispatch(modelsActions.getModelsFailed(error)); return; } - dispatch(getModelsSuccess(models)); + dispatch(modelsActions.getModelsSuccess(models)); }; } -function deleteModel(id: number): AnyAction { - const action = { - type: ModelsActionTypes.DELETE_MODEL, - payload: { - id, - }, - }; - - return action; -} - -function deleteModelSuccess(id: number): AnyAction { - const action = { - type: ModelsActionTypes.DELETE_MODEL_SUCCESS, - payload: { - id, - }, - }; - - return action; -} - -function deleteModelFailed(id: number, error: any): AnyAction { - const action = { - type: ModelsActionTypes.DELETE_MODEL_FAILED, - payload: { - error, - id, - }, - }; - - return action; -} - -export function deleteModelAsync(id: number): ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - dispatch(deleteModel(id)); +export function deleteModelAsync(id: number): ThunkAction { + return async (dispatch): Promise => { try { await core.server.request(`${baseURL}/auto_annotation/delete/${id}`, { method: 'DELETE', }); } catch (error) { - dispatch(deleteModelFailed(id, error)); + dispatch(modelsActions.deleteModelFailed(id, error)); return; } - dispatch(deleteModelSuccess(id)); - }; -} - - -function createModel(): AnyAction { - const action = { - type: ModelsActionTypes.CREATE_MODEL, - payload: {}, - }; - - return action; -} - -function createModelSuccess(): AnyAction { - const action = { - type: ModelsActionTypes.CREATE_MODEL_SUCCESS, - payload: {}, + dispatch(modelsActions.deleteModelSuccess(id)); }; - - return action; } -function createModelFailed(error: any): AnyAction { - const action = { - type: ModelsActionTypes.CREATE_MODEL_FAILED, - payload: { - error, - }, - }; - - return action; -} - -function createModelUpdateStatus(status: string): AnyAction { - const action = { - type: ModelsActionTypes.CREATE_MODEL_STATUS_UPDATED, - payload: { - status, - }, - }; - - return action; -} - -export function createModelAsync(name: string, files: ModelFiles, global: boolean): -ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { +export function createModelAsync(name: string, files: ModelFiles, global: boolean): ThunkAction { + return async (dispatch): Promise => { async function checkCallback(id: string): Promise { try { const data = await core.server.request( @@ -282,30 +243,30 @@ ThunkAction, {}, {}, AnyAction> { switch (data.status) { case 'failed': - dispatch(createModelFailed( + dispatch(modelsActions.createModelFailed( `Checking request has returned the "${data.status}" status. Message: ${data.error}`, )); break; case 'unknown': - dispatch(createModelFailed( + dispatch(modelsActions.createModelFailed( `Checking request has returned the "${data.status}" status.`, )); break; case 'finished': - dispatch(createModelSuccess()); + dispatch(modelsActions.createModelSuccess()); break; default: if ('progress' in data) { - createModelUpdateStatus(data.progress); + modelsActions.createModelUpdateStatus(data.progress); } setTimeout(checkCallback.bind(null, id), 1000); } } catch (error) { - dispatch(createModelFailed(error)); + dispatch(modelsActions.createModelFailed(error)); } } - dispatch(createModel()); + dispatch(modelsActions.createModel()); const data = new FormData(); data.append('name', name); data.append('storage', typeof files.bin === 'string' ? 'shared' : 'local'); @@ -316,7 +277,7 @@ ThunkAction, {}, {}, AnyAction> { }, data); try { - dispatch(createModelUpdateStatus('Request is beign sent..')); + dispatch(modelsActions.createModelUpdateStatus('Request is beign sent..')); const response = await core.server.request( `${baseURL}/auto_annotation/create`, { method: 'POST', @@ -326,56 +287,19 @@ ThunkAction, {}, {}, AnyAction> { }, ); - dispatch(createModelUpdateStatus('Request is being processed..')); + dispatch(modelsActions.createModelUpdateStatus('Request is being processed..')); setTimeout(checkCallback.bind(null, response.id), 1000); } catch (error) { - dispatch(createModelFailed(error)); + dispatch(modelsActions.createModelFailed(error)); } }; } -function fetchMetaFailed(error: any): AnyAction { - const action = { - type: ModelsActionTypes.FETCH_META_FAILED, - payload: { - error, - }, - }; - - return action; -} - -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; + modelType: ModelType; } const timers: any = {}; @@ -383,7 +307,8 @@ const timers: any = {}; async function timeoutCallback( url: string, taskID: number, - dispatch: ActionCreator, + modelType: ModelType, + dispatch: (action: ModelsActions) => void, ): Promise { try { delete timers[taskID]; @@ -396,11 +321,12 @@ async function timeoutCallback( status: response.status, progress: +response.progress || 0, error: response.error || response.stderr || '', + modelType, }; if (activeInference.status === 'unknown') { - dispatch(getInferenceStatusFailed( + dispatch(modelsActions.getInferenceStatusFailed( taskID, new Error( `Inference status for the task ${taskID} is unknown.`, @@ -411,7 +337,7 @@ async function timeoutCallback( } if (activeInference.status === 'failed') { - dispatch(getInferenceStatusFailed( + dispatch(modelsActions.getInferenceStatusFailed( taskID, new Error( `Inference status for the task ${taskID} is failed. ${activeInference.error}`, @@ -427,55 +353,67 @@ async function timeoutCallback( null, url, taskID, + modelType, dispatch, ), 3000, ); } - dispatch(getInferenceStatusSuccess(taskID, activeInference)); + dispatch(modelsActions.getInferenceStatusSuccess(taskID, activeInference)); } catch (error) { - dispatch(getInferenceStatusFailed(taskID, new Error( + dispatch(modelsActions.getInferenceStatusFailed(taskID, new Error( `Server request for the task ${taskID} was failed`, ))); } } function subscribe( - urlPath: string, inferenceMeta: InferenceMeta, - dispatch: ActionCreator, + dispatch: (action: ModelsActions) => void, ): void { if (!(inferenceMeta.taskID in timers)) { - const requestURL = `${baseURL}/${urlPath}/${inferenceMeta.requestID}`; + let requestURL = `${baseURL}`; + if (inferenceMeta.modelType === ModelType.OPENVINO) { + requestURL = `${requestURL}/auto_annotation/check`; + } else if (inferenceMeta.modelType === ModelType.RCNN) { + requestURL = `${requestURL}/tensorflow/annotation/check/task`; + } else if (inferenceMeta.modelType === ModelType.MASK_RCNN) { + requestURL = `${requestURL}/tensorflow/segmentation/check/task`; + } + requestURL = `${requestURL}/${inferenceMeta.requestID}`; timers[inferenceMeta.taskID] = setTimeout( timeoutCallback.bind( null, requestURL, inferenceMeta.taskID, + inferenceMeta.modelType, dispatch, ), ); } } -export function getInferenceStatusAsync(tasks: number[]): -ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - function parse(response: any): InferenceMeta[] { +export function getInferenceStatusAsync(tasks: number[]): ThunkAction { + return async (dispatch, getState): Promise => { + function parse(response: any, modelType: ModelType): 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, + modelType, })); } - const store = getCVATStore(); - const state: CombinedState = store.getState(); + const state: CombinedState = getState(); const OpenVINO = state.plugins.list.AUTO_ANNOTATION; const RCNN = state.plugins.list.TF_ANNOTATION; const MaskRCNN = state.plugins.list.TF_SEGMENTATION; + const dispatchCallback = (action: ModelsActions): void => { + dispatch(action); + }; + try { if (OpenVINO) { const response = await core.server.request( @@ -488,10 +426,10 @@ ThunkAction, {}, {}, AnyAction> { }, ); - parse(response.run) + parse(response.run, ModelType.OPENVINO) .filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active) .forEach((inferenceMeta: InferenceMeta): void => { - subscribe('auto_annotation/check', inferenceMeta, dispatch); + subscribe(inferenceMeta, dispatchCallback); }); } @@ -506,10 +444,10 @@ ThunkAction, {}, {}, AnyAction> { }, ); - parse(response) + parse(response, ModelType.RCNN) .filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active) .forEach((inferenceMeta: InferenceMeta): void => { - subscribe('tensorflow/annotation/check/task', inferenceMeta, dispatch); + subscribe(inferenceMeta, dispatchCallback); }); } @@ -524,60 +462,27 @@ ThunkAction, {}, {}, AnyAction> { }, ); - parse(response) + parse(response, ModelType.MASK_RCNN) .filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active) .forEach((inferenceMeta: InferenceMeta): void => { - subscribe('tensorflow/segmentation/check/task', inferenceMeta, dispatch); + subscribe(inferenceMeta, dispatchCallback); }); } } catch (error) { - dispatch(fetchMetaFailed(error)); + dispatch(modelsActions.fetchMetaFailed(error)); } }; } - -function inferModel(): AnyAction { - const action = { - type: ModelsActionTypes.INFER_MODEL, - payload: {}, - }; - - return action; -} - -function inferModelSuccess(): AnyAction { - const action = { - type: ModelsActionTypes.INFER_MODEL_SUCCESS, - payload: {}, - }; - - return action; -} - -function inferModelFailed(error: any, taskID: number): AnyAction { - const action = { - type: ModelsActionTypes.INFER_MODEL_FAILED, - payload: { - taskID, - error, - }, - }; - - return action; -} - -export function inferModelAsync( +export function startInferenceAsync( taskInstance: any, model: Model, mapping: { [index: string]: string; }, cleanOut: boolean, -): ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - dispatch(inferModel()); - +): ThunkAction { + return async (dispatch): Promise => { try { if (model.name === PreinstalledModels.RCNN) { await core.server.request( @@ -604,30 +509,39 @@ export function inferModelAsync( dispatch(getInferenceStatusAsync([taskInstance.id])); } catch (error) { - dispatch(inferModelFailed(error, taskInstance.id)); - return; + dispatch(modelsActions.startInferenceFailed(taskInstance.id, error)); } - - dispatch(inferModelSuccess()); }; } -export function closeRunModelDialog(): AnyAction { - const action = { - type: ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG, - payload: {}, - }; +export function cancelInferenceAsync(taskID: number): ThunkAction { + return async (dispatch, getState): Promise => { + try { + const inference = getState().models.inferences[taskID]; + if (inference) { + if (inference.modelType === ModelType.OPENVINO) { + await core.server.request( + `${baseURL}/auto_annotation/cancel/${taskID}`, + ); + } else if (inference.modelType === ModelType.RCNN) { + await core.server.request( + `${baseURL}/tensorflow/annotation/cancel/task/${taskID}`, + ); + } else if (inference.modelType === ModelType.MASK_RCNN) { + await core.server.request( + `${baseURL}/tensorflow/segmentation/cancel/task/${taskID}`, + ); + } - return action; -} + if (timers[taskID]) { + clearTimeout(timers[taskID]); + delete timers[taskID]; + } + } -export function showRunModelDialog(taskInstance: any): AnyAction { - const action = { - type: ModelsActionTypes.SHOW_RUN_MODEL_DIALOG, - payload: { - taskInstance, - }, + dispatch(modelsActions.cancelInferenceSuccess(taskID)); + } catch (error) { + dispatch(modelsActions.cancelInferenceFaild(taskID, error)); + } }; - - return action; } diff --git a/cvat-ui/src/components/tasks-page/styles.scss b/cvat-ui/src/components/tasks-page/styles.scss index a203faaa..b2fbc9be 100644 --- a/cvat-ui/src/components/tasks-page/styles.scss +++ b/cvat-ui/src/components/tasks-page/styles.scss @@ -87,37 +87,11 @@ padding-top: 20px; background: $background-color-1; - /* description */ - > div:nth-child(2) { - word-break: break-all; - max-height: 100%; - overflow: hidden; - } - - /* open, actions */ - div:nth-child(4) { - > div { - margin-right: 20px; - } - - /* actions */ - > div:nth-child(2) { - margin-right: 5px; - margin-top: 10px; - - > div { - display: flex; - align-items: center; - } - } - } - &:hover { border: 1px solid $border-color-hover; } } - .cvat-task-item-preview-wrapper { display: flex; justify-content: center; @@ -131,6 +105,12 @@ } } +.cvat-task-item-description { + word-break: break-all; + max-height: 100%; + overflow: hidden; +} + .cvat-task-progress { width: 100%; } @@ -159,6 +139,26 @@ margin-right: 5px; } +.close-auto-annotation-icon { + color: $danger-icon-color; + opacity: 0.7; + + &:hover { + opacity: 1; + } +} + +.cvat-item-open-task-actions { + margin-right: 5px; + margin-top: 10px; + display: flex; + align-items: center; +} + +.cvat-item-open-task-button { + margin-right: 20px; +} + #cvat-create-task-button { padding: 0 30px; } diff --git a/cvat-ui/src/components/tasks-page/task-item.tsx b/cvat-ui/src/components/tasks-page/task-item.tsx index 75b54c40..915544f6 100644 --- a/cvat-ui/src/components/tasks-page/task-item.tsx +++ b/cvat-ui/src/components/tasks-page/task-item.tsx @@ -14,6 +14,8 @@ import { Icon, Progress, Dropdown, + Tooltip, + Modal, } from 'antd'; import moment from 'moment'; @@ -28,6 +30,7 @@ export interface TaskItemProps { deleted: boolean; hidden: boolean; activeInference: ActiveInference | null; + cancelAutoAnnotation(): void; } class TaskItemComponent extends React.PureComponent { @@ -54,7 +57,7 @@ class TaskItemComponent extends React.PureComponent 70 ? '...' : ''}`; return ( - + {`#${id}: `} {name}
@@ -76,6 +79,7 @@ class TaskItemComponent extends React.PureComponentAutomatic annotation - - + + + + + { + Modal.confirm({ + title: 'You are going to cancel automatic annotation?', + content: 'Reached progress will be lost. Continue?', + okType: 'danger', + onOk() { + cancelAutoAnnotation(); + }, + }); + }} + /> + + )} @@ -164,6 +185,7 @@ class TaskItemComponent extends React.PureComponent