From 6a59e7cff876246368923f857fe353a7411a199e Mon Sep 17 00:00:00 2001 From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Date: Tue, 3 Mar 2020 21:00:27 +0300 Subject: [PATCH 1/8] React UI: batch of fixes (#1227) * Fix: keyframes navigation * Fix: handled removing of the latest keyframe * Fix: activating a shape when another shape is being changed * Fix: up points in the side bar on points click * Fix: editable shape isn't transformed when change zoom * Updated message --- cvat-canvas/src/typescript/canvas.ts | 7 +++++++ cvat-canvas/src/typescript/canvasView.ts | 15 ++++++++++----- cvat-canvas/src/typescript/editHandler.ts | 14 ++++++++++---- cvat-core/src/annotations-objects.js | 10 ++++++++-- cvat-core/src/object-state.js | 2 +- cvat-ui/src/cvat-canvas.ts | 2 ++ cvat-ui/src/reducers/annotation-reducer.ts | 14 +++++++++++--- 7 files changed, 49 insertions(+), 15 deletions(-) diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts index c53c9d52..eef1de40 100644 --- a/cvat-canvas/src/typescript/canvas.ts +++ b/cvat-canvas/src/typescript/canvas.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT import { + Mode, DrawData, MergeData, SplitData, @@ -51,6 +52,7 @@ interface Canvas { dragCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void; + mode(): void; cancel(): void; } @@ -132,6 +134,10 @@ class CanvasImpl implements Canvas { this.model.select(objectState); } + public mode(): Mode { + return this.model.mode; + } + public cancel(): void { this.model.cancel(); } @@ -141,4 +147,5 @@ export { CanvasImpl as Canvas, CanvasVersion, RectDrawingMethod, + Mode as CanvasMode, }; diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 63b26e1d..def0db97 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -469,21 +469,21 @@ export class CanvasViewImpl implements CanvasView, Listener { 'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale, }); - circle.node.addEventListener('mouseenter', (): void => { + circle.on('mouseenter', (): void => { circle.attr({ 'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / self.geometry.scale, }); - circle.node.addEventListener('dblclick', dblClickHandler); + circle.on('dblclick', dblClickHandler); circle.addClass('cvat_canvas_selected_point'); }); - circle.node.addEventListener('mouseleave', (): void => { + circle.on('mouseleave', (): void => { circle.attr({ 'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale, }); - circle.node.removeEventListener('dblclick', dblClickHandler); + circle.off('dblclick', dblClickHandler); circle.removeClass('cvat_canvas_selected_point'); }); @@ -1330,13 +1330,18 @@ export class CanvasViewImpl implements CanvasView, Listener { private setupPoints(basicPolyline: SVG.PolyLine, state: any): any { this.selectize(true, basicPolyline); - const group = basicPolyline.remember('_selectHandler').nested + const group: SVG.G = basicPolyline.remember('_selectHandler').nested .addClass('cvat_canvas_shape').attr({ clientID: state.clientID, id: `cvat_canvas_shape_${state.clientID}`, 'data-z-order': state.zOrder, }); + group.on('click.canvas', (event: MouseEvent): void => { + // Need to redispatch the event on another element + basicPolyline.fire(new MouseEvent('click', event)); + }); + group.bbox = basicPolyline.bbox.bind(basicPolyline); group.clone = basicPolyline.clone.bind(basicPolyline); diff --git a/cvat-canvas/src/typescript/editHandler.ts b/cvat-canvas/src/typescript/editHandler.ts index 7734bc89..defdb69e 100644 --- a/cvat-canvas/src/typescript/editHandler.ts +++ b/cvat-canvas/src/typescript/editHandler.ts @@ -84,7 +84,7 @@ export class EditHandlerImpl implements EditHandler { }).draw(dummyEvent, { snapToGrid: 0.1 }); if (this.editData.state.shapeType === 'points') { - this.editLine.style('stroke-width', 0); + this.editLine.attr('stroke-width', 0); (this.editLine as any).draw('undo'); } @@ -168,7 +168,7 @@ export class EditHandlerImpl implements EditHandler { for (const points of [firstPart, secondPart]) { this.clones.push(this.canvas.polygon(points.join(' ')) .attr('fill', this.editedShape.attr('fill')) - .style('fill-opacity', '0.5') + .attr('fill-opacity', '0.5') .addClass('cvat_canvas_shape')); } @@ -340,10 +340,16 @@ export class EditHandlerImpl implements EditHandler { public transform(geometry: Geometry): void { this.geometry = geometry; + if (this.editedShape) { + this.editedShape.attr({ + 'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale, + }); + } + if (this.editLine) { (this.editLine as any).draw('transform'); if (this.editData.state.shapeType !== 'points') { - this.editLine.style({ + this.editLine.attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale, }); } @@ -351,7 +357,7 @@ export class EditHandlerImpl implements EditHandler { const paintHandler = this.editLine.remember('_paintHandler'); for (const point of (paintHandler as any).set.members) { - point.style( + point.attr( 'stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`, ); diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index f86e92ee..65a30b56 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -339,6 +339,11 @@ if (updated.keyframe) { checkObjectType('keyframe', data.keyframe, 'boolean', null); + if (!this.shapes || (Object.keys(this.shapes).length === 1 && !data.keyframe)) { + throw new ArgumentError( + 'Can not remove the latest keyframe of an object. Consider removing the object instead', + ); + } } return fittedPoints; @@ -964,7 +969,8 @@ const current = this.get(frame); const wasKeyframe = frame in this.shapes; - if ((keyframe && wasKeyframe) || (!keyframe && !wasKeyframe)) { + if ((keyframe && wasKeyframe) + || (!keyframe && !wasKeyframe)) { return; } @@ -1088,7 +1094,7 @@ throw new DataError( 'No one left position or right position was found. ' - + `Interpolation impossible. Client ID: ${this.id}`, + + `Interpolation impossible. Client ID: ${this.clientID}`, ); } } diff --git a/cvat-core/src/object-state.js b/cvat-core/src/object-state.js index 806a2909..e6a42f18 100644 --- a/cvat-core/src/object-state.js +++ b/cvat-core/src/object-state.js @@ -39,7 +39,7 @@ color: null, hidden: null, pinned: null, - keyframes: null, + keyframes: serialized.keyframes, group: serialized.group, updated: serialized.updated, diff --git a/cvat-ui/src/cvat-canvas.ts b/cvat-ui/src/cvat-canvas.ts index 874637ad..6317c435 100644 --- a/cvat-ui/src/cvat-canvas.ts +++ b/cvat-ui/src/cvat-canvas.ts @@ -4,12 +4,14 @@ import { Canvas, + CanvasMode, CanvasVersion, RectDrawingMethod, } from '../../cvat-canvas/src/typescript/canvas'; export { Canvas, + CanvasMode, CanvasVersion, RectDrawingMethod, }; diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index b9615fe7..5e021d63 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -4,7 +4,7 @@ import { AnyAction } from 'redux'; -import { Canvas } from 'cvat-canvas'; +import { Canvas, CanvasMode } from 'cvat-canvas'; import { AnnotationActionTypes } from 'actions/annotation-actions'; import { AuthActionTypes } from 'actions/auth-actions'; import { @@ -609,9 +609,17 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }; } case AnnotationActionTypes.ACTIVATE_OBJECT: { + const { activatedStateID } = action.payload; const { - activatedStateID, - } = action.payload; + canvas: { + activeControl, + instance, + }, + } = state; + + if (activeControl !== ActiveControl.CURSOR || instance.mode() !== CanvasMode.IDLE) { + return state; + } return { ...state, From 401d66dcd711bdac959da42ade36a0e0dd1eb9ba Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 3 Mar 2020 22:30:52 +0300 Subject: [PATCH 2/8] React UI: Filters history (#1225) * Added filters history * Fixed unclosed dropdown * Added saving filters to localStrorage --- cvat-canvas/src/typescript/canvasView.ts | 1 - cvat-ui/src/actions/annotation-actions.ts | 12 ++++++++++++ .../objects-side-bar/objects-list-header.tsx | 9 +++++++-- .../objects-side-bar/objects-list.tsx | 3 +++ .../objects-side-bar/objects-list.tsx | 9 ++++++++- cvat-ui/src/reducers/annotation-reducer.ts | 5 ++++- cvat-ui/src/reducers/interfaces.ts | 1 + 7 files changed, 35 insertions(+), 5 deletions(-) diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index def0db97..d026c524 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -629,7 +629,6 @@ export class CanvasViewImpl implements CanvasView, Listener { if (![Mode.ZOOM_CANVAS, Mode.GROUP].includes(this.mode) || event.which === 2) { self.controller.enableDrag(event.clientX, event.clientY); } - event.preventDefault(); } }); diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index bc49fb02..2ee2b011 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -179,10 +179,22 @@ ThunkAction, {}, {}, AnyAction> { } export function changeAnnotationsFilters(filters: string[]): AnyAction { + const state: CombinedState = getStore().getState(); + const { filtersHistory, filters: oldFilters } = state.annotation.annotations; + + filters.forEach((element: string) => { + if (!(filtersHistory.includes(element) || oldFilters.includes(element))) { + filtersHistory.push(element); + } + }); + + window.localStorage.setItem('filtersHistory', JSON.stringify(filtersHistory.slice(-10))); + return { type: AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS, payload: { filters, + filtersHistory: filtersHistory.slice(-10), }, }; } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx index 8bc8eba7..d7406ccd 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx @@ -63,6 +63,7 @@ interface Props { statesCollapsed: boolean; statesOrdering: StatesOrdering; annotationsFilters: string[]; + annotationsFiltersHistory: string[]; changeStatesOrdering(value: StatesOrdering): void; changeAnnotationsFilters(value: SelectValue): void; lockAllStates(): void; @@ -76,6 +77,7 @@ interface Props { function ObjectListHeader(props: Props): JSX.Element { const { annotationsFilters, + annotationsFiltersHistory, statesHidden, statesLocked, statesCollapsed, @@ -105,9 +107,12 @@ function ObjectListHeader(props: Props): JSX.Element { Annotations filter )} - dropdownStyle={{ display: 'none' }} onChange={changeAnnotationsFilters} - /> + > + {annotationsFiltersHistory.map((element: string): JSX.Element => ( + {element} + ))} + diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx index b5942646..e3af2e3c 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx @@ -18,6 +18,7 @@ interface Props { statesOrdering: StatesOrdering; sortedStatesID: number[]; annotationsFilters: string[]; + annotationsFiltersHistory: string[]; changeStatesOrdering(value: StatesOrdering): void; changeAnnotationsFilters(value: SelectValue): void; lockAllStates(): void; @@ -37,6 +38,7 @@ function ObjectListComponent(props: Props): JSX.Element { statesOrdering, sortedStatesID, annotationsFilters, + annotationsFiltersHistory, changeStatesOrdering, changeAnnotationsFilters, lockAllStates, @@ -63,6 +65,7 @@ function ObjectListComponent(props: Props): JSX.Element { expandAllStates={expandAllStates} hideAllStates={hideAllStates} showAllStates={showAllStates} + annotationsFiltersHistory={annotationsFiltersHistory} />
{ sortedStatesID.map((id: number): JSX.Element => ( diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx index b1e2ed85..0c22f1f5 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx @@ -29,6 +29,7 @@ interface StateToProps { statesCollapsed: boolean; objectStates: any[]; annotationsFilters: string[]; + annotationsFiltersHistory: string[]; } interface DispatchToProps { @@ -43,6 +44,7 @@ function mapStateToProps(state: CombinedState): StateToProps { annotations: { states: objectStates, filters: annotationsFilters, + filtersHistory: annotationsFiltersHistory, collapsed, }, job: { @@ -78,6 +80,7 @@ function mapStateToProps(state: CombinedState): StateToProps { frameNumber, jobInstance, annotationsFilters, + annotationsFiltersHistory, }; } @@ -221,7 +224,10 @@ class ObjectsListContainer extends React.PureComponent { } public render(): JSX.Element { - const { annotationsFilters } = this.props; + const { + annotationsFilters, + annotationsFiltersHistory, + } = this.props; const { sortedStatesID, statesOrdering, @@ -233,6 +239,7 @@ class ObjectsListContainer extends React.PureComponent { statesOrdering={statesOrdering} sortedStatesID={sortedStatesID} annotationsFilters={annotationsFilters} + annotationsFiltersHistory={annotationsFiltersHistory} changeStatesOrdering={this.onChangeStatesOrdering} changeAnnotationsFilters={this.onChangeAnnotationsFilters} lockAllStates={this.onLockAllStates} diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 5e021d63..ce857147 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -61,6 +61,7 @@ const defaultState: AnnotationState = { collapsed: {}, states: [], filters: [], + filtersHistory: JSON.parse(window.localStorage.getItem('filtersHistory') as string) || [], history: { undo: [], redo: [], @@ -954,11 +955,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }; } case AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS: { - const { filters } = action.payload; + const { filters, filtersHistory } = action.payload; + return { ...state, annotations: { ...state.annotations, + filtersHistory, filters, }, }; diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 644d85d5..ebf6b506 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -328,6 +328,7 @@ export interface AnnotationState { collapsed: Record; states: any[]; filters: string[]; + filtersHistory: string[]; history: { undo: string[]; redo: string[]; From 65cbabd8df3febd8fd16f865cc9d38baf090bce0 Mon Sep 17 00:00:00 2001 From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Date: Tue, 3 Mar 2020 23:52:27 +0300 Subject: [PATCH 3/8] Added button to cancel started automatic annotation (#1198) --- cvat-ui/src/actions/models-actions.ts | 430 +++++++----------- cvat-ui/src/components/tasks-page/styles.scss | 52 +-- .../src/components/tasks-page/task-item.tsx | 30 +- .../containers/actions-menu/actions-menu.tsx | 4 +- .../model-runner-dialog.tsx | 8 +- .../src/containers/tasks-page/task-item.tsx | 25 +- cvat-ui/src/reducers/interfaces.ts | 8 + cvat-ui/src/reducers/models-reducer.ts | 25 +- cvat-ui/src/reducers/notifications-reducer.ts | 20 +- 9 files changed, 283 insertions(+), 319 deletions(-) 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