From 9f63686baf49c3d2dbc3de62b9e102247f42ab34 Mon Sep 17 00:00:00 2001 From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Date: Tue, 26 Nov 2019 15:15:01 +0300 Subject: [PATCH] React & Antd UI: Model manager (#856) * Supported git to create and sync * Updated antd * Updated icons * Improved header * Top bar for models & empty models list * Removed one extra reducer and actions * Removed one extra reducer and actions * Crossplatform css * Models reducers, some models actions, base for model list, imrovements * Models list, ability to delete models * Added ability to upload models * Improved form, reinit models after create * Removed some importants in css * Model running dialog window, a lot of fixes --- cvat-core/src/api-implementation.js | 5 + cvat-core/src/api.js | 16 + cvat-core/src/server-proxy.js | 12 + cvat-ui/package-lock.json | 94 ++-- cvat-ui/package.json | 2 +- cvat-ui/src/actions/models-actions.ts | 417 ++++++++++++++++++ cvat-ui/src/actions/task-actions.ts | 145 ------ cvat-ui/src/actions/tasks-actions.ts | 98 ++++ .../components/actions-menu/actions-menu.tsx | 37 +- .../components/actions-menu/dumper-item.tsx | 4 +- .../components/actions-menu/loader-item.tsx | 4 +- .../create-model-content.tsx | 139 ++++++ .../create-model-page/create-model-form.tsx | 85 ++++ .../create-model-page/create-model-page.tsx | 34 ++ .../advanced-configuration-form.tsx | 33 +- .../basic-configuration-form.tsx | 6 +- .../create-task-page/create-task-content.tsx | 12 +- .../create-task-page/create-task-page.tsx | 3 +- cvat-ui/src/components/cvat-app.tsx | 20 +- .../components/file-manager/file-manager.tsx | 7 +- cvat-ui/src/components/header/header.tsx | 136 +++--- .../components/labels-editor/label-form.tsx | 2 +- .../labels-editor/labels-editor.tsx | 4 +- .../model-runner-modal/model-runner-modal.tsx | 340 ++++++++++++++ .../models-page/built-model-item.tsx | 46 ++ .../models-page/built-models-list.tsx | 47 ++ .../src/components/models-page/empty-list.tsx | 39 ++ .../components/models-page/models-page.tsx | 54 +++ .../src/components/models-page/top-bar.tsx | 39 ++ .../models-page/uploaded-model-item.tsx | 76 ++++ .../models-page/uploaded-models-list.tsx | 64 +++ cvat-ui/src/components/task-page/details.tsx | 102 ++++- .../src/components/task-page/task-page.tsx | 22 +- cvat-ui/src/components/task-page/top-bar.tsx | 33 +- .../src/components/tasks-page/empty-list.tsx | 6 +- .../src/components/tasks-page/task-item.tsx | 38 +- .../src/components/tasks-page/tasks-page.tsx | 6 + cvat-ui/src/components/tasks-page/top-bar.tsx | 62 ++- .../containers/actions-menu/actions-menu.tsx | 91 ++++ .../create-model-page/create-model-page.tsx | 53 +++ .../create-task-page/create-task-page.tsx | 2 +- .../containers/file-manager/file-manager.tsx | 15 +- cvat-ui/src/containers/header/header.tsx | 12 +- .../src/containers/login-page/login-page.tsx | 2 +- .../model-runner-dialog.tsx | 93 ++++ .../src/containers/models-page/empty-page.tsx | 0 .../containers/models-page/models-page.tsx | 88 +++- .../register-page/register-page.tsx | 2 +- cvat-ui/src/containers/task-page/details.tsx | 25 +- cvat-ui/src/containers/task-page/job-list.tsx | 17 +- .../src/containers/task-page/task-page.tsx | 52 ++- cvat-ui/src/containers/task-page/top-bar.tsx | 86 ---- .../src/containers/tasks-page/task-item.tsx | 46 +- .../src/containers/tasks-page/tasks-list.tsx | 5 +- .../src/containers/tasks-page/tasks-page.tsx | 2 +- cvat-ui/src/index.tsx | 18 +- cvat-ui/src/reducers/interfaces.ts | 59 ++- cvat-ui/src/reducers/models-reducer.ts | 125 ++++++ cvat-ui/src/reducers/plugins-reducer.ts | 10 +- cvat-ui/src/reducers/root-reducer.ts | 24 +- cvat-ui/src/reducers/task-reducer.ts | 65 --- cvat-ui/src/reducers/tasks-reducer.ts | 43 ++ cvat-ui/src/store.ts | 23 +- cvat-ui/src/stylesheet.css | 359 +++++++++++++-- cvat-ui/src/utils/git-utils.ts | 191 ++++++++ cvat-ui/src/utils/plugin-checker.ts | 7 + cvat-ui/src/utils/validation-patterns.ts | 9 +- cvat/apps/auto_annotation/views.py | 1 + cvat/apps/git/git.py | 3 +- 69 files changed, 3051 insertions(+), 766 deletions(-) create mode 100644 cvat-ui/src/actions/models-actions.ts delete mode 100644 cvat-ui/src/actions/task-actions.ts create mode 100644 cvat-ui/src/components/create-model-page/create-model-content.tsx create mode 100644 cvat-ui/src/components/create-model-page/create-model-form.tsx create mode 100644 cvat-ui/src/components/create-model-page/create-model-page.tsx create mode 100644 cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx create mode 100644 cvat-ui/src/components/models-page/built-model-item.tsx create mode 100644 cvat-ui/src/components/models-page/built-models-list.tsx create mode 100644 cvat-ui/src/components/models-page/empty-list.tsx create mode 100644 cvat-ui/src/components/models-page/models-page.tsx create mode 100644 cvat-ui/src/components/models-page/top-bar.tsx create mode 100644 cvat-ui/src/components/models-page/uploaded-model-item.tsx create mode 100644 cvat-ui/src/components/models-page/uploaded-models-list.tsx create mode 100644 cvat-ui/src/containers/actions-menu/actions-menu.tsx create mode 100644 cvat-ui/src/containers/create-model-page/create-model-page.tsx create mode 100644 cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx delete mode 100644 cvat-ui/src/containers/models-page/empty-page.tsx delete mode 100644 cvat-ui/src/containers/task-page/top-bar.tsx create mode 100644 cvat-ui/src/reducers/models-reducer.ts delete mode 100644 cvat-ui/src/reducers/task-reducer.ts create mode 100644 cvat-ui/src/utils/git-utils.ts diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 72213c942a0..8344039b073 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -89,6 +89,11 @@ return result; }; + cvat.server.request.implementation = async (url, data) => { + const result = await serverProxy.server.request(url, data); + return result; + }; + cvat.users.get.implementation = async (filter) => { checkFilter(filter, { self: isBoolean, diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 21c2299a249..ddfa18077f5 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -177,6 +177,22 @@ function build() { .apiWrapper(cvat.server.authorized); return result; }, + /** + * Method allows to do requests via cvat-core with authorization headers + * @method request + * @async + * @memberof module:API.cvat.server + * @param {string} url + * @param {Object} data request parameters: method, headers, data, etc. + * @returns {Object | undefined} response data if exist + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + */ + async request(url, data) { + const result = await PluginRegistry + .apiWrapper(cvat.server.request, url, data); + return result; + }, }, /** * Namespace is used for getting tasks diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 143cb8e1659..0ac32852a2f 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -182,6 +182,17 @@ return true; } + async function serverRequest(url, data) { + try { + return (await Axios({ + url, + ...data, + })).data; + } catch (errorData) { + throw generateError(errorData, 'Could not have done the request'); + } + } + async function getTasks(filter = '') { const { backendAPI } = config; @@ -560,6 +571,7 @@ logout, authorized, register, + request: serverRequest, }), writable: false, }, diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index cdde8457439..9422a2bc845 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1390,9 +1390,9 @@ } }, "antd": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/antd/-/antd-3.24.3.tgz", - "integrity": "sha512-yr4kV8lUdYNCOj5+TjIufLGYF0naTfNiAJV0JWqh9RzRGapGVES928K/2gyF7Ow3da6ZlsgxtO1P25exbvDlSw==", + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/antd/-/antd-3.25.2.tgz", + "integrity": "sha512-+qF1bgU7rUkPIkggIIV0fmm+9pPacl50BBd6NNUR2+kKJOFYjwrnP39ZqJRsYNy5bX9VgR454fz9KEuW7HPjog==", "requires": { "@ant-design/create-react-context": "^0.2.4", "@ant-design/icons": "~2.1.1", @@ -1410,7 +1410,7 @@ "omit.js": "^1.0.2", "prop-types": "^15.7.2", "raf": "^3.4.1", - "rc-animate": "^2.8.3", + "rc-animate": "^2.10.2", "rc-calendar": "~9.15.5", "rc-cascader": "~0.17.4", "rc-checkbox": "~2.1.6", @@ -1419,7 +1419,7 @@ "rc-drawer": "~3.0.0", "rc-dropdown": "~2.4.1", "rc-editor-mention": "^1.1.13", - "rc-form": "^2.4.5", + "rc-form": "^2.4.10", "rc-input-number": "~4.5.0", "rc-mentions": "~0.4.0", "rc-menu": "~7.5.1", @@ -1439,7 +1439,7 @@ "rc-tree": "~2.1.0", "rc-tree-select": "~2.9.1", "rc-trigger": "^2.6.2", - "rc-upload": "~2.8.0", + "rc-upload": "~2.9.1", "rc-util": "^4.10.0", "react-lazy-load": "^3.0.13", "react-lifecycles-compat": "^3.0.4", @@ -5862,36 +5862,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" - }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, "lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -7390,23 +7365,23 @@ } }, "rc-animate": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.10.1.tgz", - "integrity": "sha512-yfP3g5fNf8wB5eh85nim2IGrqNu5u7TKrrSh710+1vlUqZvnI2R5YHK99IBCQNgkLCAWjT0sHtkcYdynjly39w==", + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.10.2.tgz", + "integrity": "sha512-cE/A7piAzoWFSgUD69NmmMraqCeqVBa51UErod8NS3LUEqWfppSVagHfa0qHAlwPVPiIBg3emRONyny3eiH0Dg==", "requires": { "babel-runtime": "6.x", "classnames": "^2.2.6", "css-animation": "^1.3.2", "prop-types": "15.x", "raf": "^3.4.0", - "rc-util": "^4.8.0", + "rc-util": "^4.15.3", "react-lifecycles-compat": "^3.0.4" } }, "rc-calendar": { - "version": "9.15.6", - "resolved": "https://registry.npmjs.org/rc-calendar/-/rc-calendar-9.15.6.tgz", - "integrity": "sha512-TJD4cUXsBAjyCzo7BaGb86nZyJetBUt/Rpu0H1WWhp9AJc+Tl7aj7TCD3TM5Y8Ak/yxsA8WDBMuKw1XdQMsM5g==", + "version": "9.15.8", + "resolved": "https://registry.npmjs.org/rc-calendar/-/rc-calendar-9.15.8.tgz", + "integrity": "sha512-x3zVaZSRX7FkRNKw7nz3tutwrlIrU1aqMn5GtRUmlf84GnXLtd9fuuydxeNkFWfcHry3BPSto7+r9TK2al0h+g==", "requires": { "babel-runtime": "6.x", "classnames": "2.x", @@ -7457,9 +7432,9 @@ } }, "rc-dialog": { - "version": "7.5.12", - "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-7.5.12.tgz", - "integrity": "sha512-FsZQfHBXYjBwuxUN9Cd+0n7YRSyemDtRJ9jX2a1HvIf4ajBJK9WVUiWm2+K1vZBZOciA+jm6gQETqyXzDKnwzQ==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-7.5.13.tgz", + "integrity": "sha512-tmubIipW/qoCmRlHHV8tpepDaFhuhk+SeSFSyRhNKW4mYgflsEYQmYWilyCJHy6UzKl84bSyFvJskhc1z1Hniw==", "requires": { "babel-runtime": "6.x", "rc-animate": "2.x", @@ -7519,9 +7494,9 @@ } }, "rc-form": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/rc-form/-/rc-form-2.4.9.tgz", - "integrity": "sha512-uu6wtJqSQWTFOgv1NcYJIPf7TlJHmQHbDJTBQQuQsKKap8CiW6aeAfvOZpThQuWwV/NeznP4WKeOJurIw4zzlA==", + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/rc-form/-/rc-form-2.4.10.tgz", + "integrity": "sha512-h6a5Nvn6fMe3BfLpIWwL2RUkfXs1tvtifblTgGgH0UfzGgiQ5M12jiMJaAXek7TDDBUw90/c5vlZ6wFZjW0IgQ==", "requires": { "async-validator": "~1.11.3", "babel-runtime": "6.x", @@ -7529,6 +7504,7 @@ "dom-scroll-into-view": "1.x", "hoist-non-react-statics": "^3.3.0", "lodash": "^4.17.4", + "rc-util": "^4.15.3", "warning": "^4.0.3" } }, @@ -7596,9 +7572,9 @@ } }, "rc-pagination": { - "version": "1.20.9", - "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-1.20.9.tgz", - "integrity": "sha512-X/y2kZWrUyX/x7Ncbh/KrcPxStMuQTytqx4XPsla5ub881wGpiCdiVJxfhlqlVlqJmXRsxLYAcn8Vbi8pmmjKA==", + "version": "1.20.11", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-1.20.11.tgz", + "integrity": "sha512-2wKO5kO+ELx1/zlqTY8TwGBruzofi+1BcZ7Z4xalMlLbDMTuUU4FDljbBBP/n9D2llK+NtgWA619PMBhInozZw==", "requires": { "babel-runtime": "6.x", "classnames": "^2.2.6", @@ -7851,9 +7827,9 @@ } }, "rc-upload": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-2.8.1.tgz", - "integrity": "sha512-FmKZgWsgOyKeZuperDjHrj8Qx5fdQqYuNpmDR50AP7Za87o8QsRvCbIKG2pgQ9MpNkUbiQOV15FqlQBl2WisfQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-2.9.2.tgz", + "integrity": "sha512-USjuWpTRJl3my32G5woysTaGrAld+S4dvvZ9kW6RX/RkekfmLDjvWc5ho8Mj/+6B6/tDRJnyGyvMxMQNkW7cvw==", "requires": { "babel-runtime": "6.x", "classnames": "^2.2.5", @@ -7862,25 +7838,15 @@ } }, "rc-util": { - "version": "4.14.4", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.14.4.tgz", - "integrity": "sha512-GQgEn6ywJYZq1NEoZ6NZzeaE2U6mT6DhdqrtRV5IBNM3yTZZW8HRjIiMOpXOhTEUj10bnHnKWKZpC36RoNmS9Q==", + "version": "4.15.6", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.15.6.tgz", + "integrity": "sha512-W6HB1gIn+xZLxmQfLkhMnAtaZY9RktcOH2I0Tbam4D4ZDFrkO33f3M7IolN0EPtLMpf4Mv/dEQNclY77/PtBpg==", "requires": { "add-dom-event-listener": "^1.1.0", "babel-runtime": "6.x", "prop-types": "^15.5.10", "react-lifecycles-compat": "^3.0.4", - "shallowequal": "^0.2.2" - }, - "dependencies": { - "shallowequal": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-0.2.2.tgz", - "integrity": "sha1-HjL9W8q2rWiKSBLLDMBO/HXHAU4=", - "requires": { - "lodash.keys": "^3.1.2" - } - } + "shallowequal": "^1.1.0" } }, "react": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 5781b1aa7c7..50d78f82c35 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -39,7 +39,7 @@ "@types/react-router": "^5.0.5", "@types/react-router-dom": "^5.1.0", "@types/react-share": "^3.0.1", - "antd": "^3.24.2", + "antd": "^3.25.2", "dotenv-webpack": "^1.7.0", "moment": "^2.24.0", "prop-types": "^15.7.2", diff --git a/cvat-ui/src/actions/models-actions.ts b/cvat-ui/src/actions/models-actions.ts new file mode 100644 index 00000000000..d11549d89e7 --- /dev/null +++ b/cvat-ui/src/actions/models-actions.ts @@ -0,0 +1,417 @@ +import { AnyAction, Dispatch, ActionCreator } from 'redux'; +import { ThunkAction } from 'redux-thunk'; + +import getCore from '../core'; +import { getCVATStore } from '../store'; +import { Model, ModelFiles, CombinedState } from '../reducers/interfaces'; + +export enum ModelsActionTypes { + GET_MODELS = 'GET_MODELS', + GET_MODELS_SUCCESS = 'GET_MODELS_SUCCESS', + GET_MODELS_FAILED = 'GET_MODELS_FAILED', + DELETE_MODEL = 'DELETE_MODEL', + DELETE_MODEL_SUCCESS = 'DELETE_MODEL_SUCCESS', + DELETE_MODEL_FAILED = 'DELETE_MODEL_FAILED', + CREATE_MODEL = 'CREATE_MODEL', + 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', + GET_INFERENCE_STATUS = 'GET_INFERENCE_STATUS', + GET_INFERENCE_STATUS_SUCCESS = 'GET_INFERENCE_STATUS_SUCCESS', + GET_INFERENCE_STATUS_FAILED = 'GET_INFERENCE_STATUS_FAILED', + SHOW_RUN_MODEL_DIALOG = 'SHOW_RUN_MODEL_DIALOG', + CLOSE_RUN_MODEL_DIALOG = 'CLOSE_RUN_MODEL_DIALOG', +} + +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: { + models, + }, + }; + + return action; +} + +function getModelsFailed(error: any): AnyAction { + const action = { + type: ModelsActionTypes.GET_MODELS_FAILED, + payload: { + error, + }, + }; + + return action; +} + +export function getModelsAsync(): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + 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; + + dispatch(getModels()); + const models: Model[] = []; + + try { + if (OpenVINO) { + const response = await core.server.request( + `${baseURL}/auto_annotation/meta/get`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: JSON.stringify([]), + }, + ); + + + for (const model of response.models) { + models.push({ + id: model.id, + ownerID: model.owner, + primary: model.primary, + name: model.name, + uploadDate: model.uploadDate, + updateDate: model.updateDate, + labels: [...model.labels], + }); + } + } + + if (RCNN) { + models.push({ + id: null, + ownerID: null, + primary: true, + name: PreinstalledModels.RCNN, + uploadDate: '', + updateDate: '', + labels: ['surfboard', 'car', 'skateboard', 'boat', 'clock', + 'cat', 'cow', 'knife', 'apple', 'cup', 'tv', + 'baseball_bat', 'book', 'suitcase', 'tennis_racket', + 'stop_sign', 'couch', 'cell_phone', 'keyboard', + 'cake', 'tie', 'frisbee', 'truck', 'fire_hydrant', + 'snowboard', 'bed', 'vase', 'teddy_bear', + 'toaster', 'wine_glass', 'traffic_light', + 'broccoli', 'backpack', 'carrot', 'potted_plant', + 'donut', 'umbrella', 'parking_meter', 'bottle', + 'sandwich', 'motorcycle', 'bear', 'banana', + 'person', 'scissors', 'elephant', 'dining_table', + 'toothbrush', 'toilet', 'skis', 'bowl', 'sheep', + 'refrigerator', 'oven', 'microwave', 'train', + 'orange', 'mouse', 'laptop', 'bench', 'bicycle', + 'fork', 'kite', 'zebra', 'baseball_glove', 'bus', + 'spoon', 'horse', 'handbag', 'pizza', 'sports_ball', + 'airplane', 'hair_drier', 'hot_dog', 'remote', + 'sink', 'dog', 'bird', 'giraffe', 'chair', + ], + }); + } + + if (MaskRCNN) { + models.push({ + id: null, + ownerID: null, + primary: true, + name: PreinstalledModels.MaskRCNN, + uploadDate: '', + updateDate: '', + labels: ['BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', + 'bus', 'train', 'truck', 'boat', 'traffic light', + 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', + 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', + 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', + 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', + 'kite', 'baseball bat', 'baseball glove', 'skateboard', + 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', + 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', + 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', + 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', + 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', + 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', + 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', + 'teddy bear', 'hair drier', 'toothbrush', + ], + }); + } + } catch (error) { + dispatch(getModelsFailed(error)); + return; + } + + dispatch(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)); + try { + await core.server.request(`${baseURL}/auto_annotation/delete/${id}`, { + method: 'DELETE', + }); + } catch (error) { + dispatch(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: {}, + }; + + 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 => { + async function checkCallback(id: string): Promise { + try { + const data = await core.server.request( + `${baseURL}/auto_annotation/check/${id}`, { + method: 'GET', + }, + ); + + switch (data.status) { + case 'failed': + dispatch(createModelFailed( + `Checking request has returned the "${data.status}" status. Message: ${data.error}`, + )); + break; + case 'unknown': + dispatch(createModelFailed( + `Checking request has returned the "${data.status}" status.`, + )); + break; + case 'finished': + dispatch(createModelSuccess()); + break; + default: + if ('progress' in data) { + createModelUpdateStatus(data.progress); + } + setTimeout(checkCallback.bind(null, id), 1000); + } + } catch (error) { + dispatch(createModelFailed(error)); + } + } + + dispatch(createModel()); + const data = new FormData(); + data.append('name', name); + data.append('storage', typeof files.bin === 'string' ? 'shared' : 'local'); + data.append('shared', global.toString()); + Object.keys(files).reduce((acc, key: string): FormData => { + acc.append(key, files[key]); + return acc; + }, data); + + try { + dispatch(createModelUpdateStatus('Request is beign sent..')); + const response = await core.server.request( + `${baseURL}/auto_annotation/create`, { + method: 'POST', + data, + contentType: false, + processData: false, + }, + ); + + dispatch(createModelUpdateStatus('Request is being processed..')); + setTimeout(checkCallback.bind(null, response.id), 1000); + } catch (error) { + dispatch(createModelFailed(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): AnyAction { + const action = { + type: ModelsActionTypes.INFER_MODEL_FAILED, + payload: { + error, + }, + }; + + return action; +} + +export function inferModelAsync( + taskInstance: any, + model: Model, + mapping: { + [index: string]: string; + }, + cleanOut: boolean, +): ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + dispatch(inferModel()); + + try { + if (model.name === PreinstalledModels.RCNN) { + await core.server.request( + `${baseURL}/tensorflow/annotation/create/task/${taskInstance.id}`, + ); + } else if (model.name === PreinstalledModels.MaskRCNN) { + await core.server.request( + `${baseURL}/tensorflow/segmentation/create/task/${taskInstance.id}`, + ); + } else { + await core.server.request( + `${baseURL}/auto_annotation/start/${model.id}/${taskInstance.id}`, { + method: 'POST', + data: JSON.stringify({ + reset: cleanOut, + labels: mapping, + }), + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + } + } catch (error) { + dispatch(inferModelFailed(error)); + return; + } + + dispatch(inferModelSuccess()); + }; +} + +export function closeRunModelDialog(): AnyAction { + const action = { + type: ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG, + payload: {}, + }; + + return action; +} + +export function showRunModelDialog(taskInstance: any): AnyAction { + const action = { + type: ModelsActionTypes.SHOW_RUN_MODEL_DIALOG, + payload: { + taskInstance, + }, + }; + + return action; +} diff --git a/cvat-ui/src/actions/task-actions.ts b/cvat-ui/src/actions/task-actions.ts deleted file mode 100644 index 063b60223b6..00000000000 --- a/cvat-ui/src/actions/task-actions.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { AnyAction, Dispatch, ActionCreator } from 'redux'; -import { ThunkAction } from 'redux-thunk'; - -import getCore from '../core'; - -const core = getCore(); - -export enum TaskActionTypes { - GET_TASK = 'GET_TASK', - GET_TASK_SUCCESS = 'GET_TASK_SUCCESS', - GET_TASK_FAILED = 'GET_TASK_FAILED', - UPDATE_TASK = 'UPDATE_TASK', - UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS', - UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED', -} - -function getTask(): AnyAction { - const action = { - type: TaskActionTypes.GET_TASK, - payload: {}, - }; - - return action; -} - -function getTaskSuccess(taskInstance: any, previewImage: string): AnyAction { - const action = { - type: TaskActionTypes.GET_TASK_SUCCESS, - payload: { - taskInstance, - previewImage, - }, - }; - - return action; -} - -function getTaskFailed(error: any): AnyAction { - const action = { - type: TaskActionTypes.GET_TASK_FAILED, - payload: { - error, - }, - }; - - return action; -} - -export function getTaskAsync(tid: number): -ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - dispatch(getTask()); - const taskInstance = (await core.tasks.get({ id: tid }))[0]; - if (taskInstance) { - const previewImage = await taskInstance.frames.preview(); - dispatch(getTaskSuccess(taskInstance, previewImage)); - } else { - throw Error(`Task ${tid} wasn't found on the server`); - } - } catch (error) { - dispatch(getTaskFailed(error)); - } - }; -} - -function updateTask(): AnyAction { - const action = { - type: TaskActionTypes.UPDATE_TASK, - payload: {}, - }; - - return action; -} - -function updateTaskSuccess(taskInstance: any): AnyAction { - const action = { - type: TaskActionTypes.UPDATE_TASK_SUCCESS, - payload: { - taskInstance, - }, - }; - - return action; -} - -function updateTaskFailed(error: any, taskInstance: any): AnyAction { - const action = { - type: TaskActionTypes.UPDATE_TASK_FAILED, - payload: { - error, - taskInstance, - }, - }; - - return action; -} - -export function updateTaskAsync(taskInstance: any): -ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - dispatch(updateTask()); - await taskInstance.save(); - const [task] = await core.tasks.get({ id: taskInstance.id }); - dispatch(updateTaskSuccess(task)); - } catch (error) { - // try abort all changes - let task = null; - try { - [task] = await core.tasks.get({ id: taskInstance.id }); - } catch (fetchError) { - dispatch(updateTaskFailed(error, taskInstance)); - return; - } - - dispatch(updateTaskFailed(error, task)); - } - }; -} - -// a job is a part of a task, so for simplify we consider -// updating the job as updating a task -export function updateJobAsync(jobInstance: any): -ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - dispatch(updateTask()); - await jobInstance.save(); - const [task] = await core.tasks.get({ id: jobInstance.task.id }); - dispatch(updateTaskSuccess(task)); - } catch (error) { - // try abort all changes - let task = null; - try { - [task] = await core.tasks.get({ id: jobInstance.task.id }); - } catch (fetchError) { - dispatch(updateTaskFailed(error, jobInstance.task)); - return; - } - - dispatch(updateTaskFailed(error, task)); - } - }; -} diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 46d92c0c099..b54378234ed 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -23,6 +23,9 @@ export enum TasksActionTypes { CREATE_TASK_STATUS_UPDATED = 'CREATE_TASK_STATUS_UPDATED', CREATE_TASK_SUCCESS = 'CREATE_TASK_SUCCESS', CREATE_TASK_FAILED = 'CREATE_TASK_FAILED', + UPDATE_TASK = 'UPDATE_TASK', + UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS', + UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED', } function getTasks(): AnyAction { @@ -333,6 +336,21 @@ ThunkAction, {}, {}, AnyAction> { taskInstance.serverFiles = data.files.share; taskInstance.remoteFiles = data.files.remote; + if (data.advanced.repository) { + const [gitPlugin] = (await cvat.plugins.list()).filter( + (plugin: any): boolean => plugin.name === 'Git', + ); + + if (gitPlugin) { + gitPlugin.callbacks.onStatusChange = (status: string): void => { + dispatch(createTaskUpdateStatus(status)); + }; + gitPlugin.data.task = taskInstance; + gitPlugin.data.repos = data.advanced.repository; + gitPlugin.data.lfs = data.advanced.lfs; + } + } + dispatch(createTask()); try { await taskInstance.save((status: string): void => { @@ -344,3 +362,83 @@ ThunkAction, {}, {}, AnyAction> { } }; } + +function updateTask(): AnyAction { + const action = { + type: TasksActionTypes.UPDATE_TASK, + payload: {}, + }; + + return action; +} + +function updateTaskSuccess(taskInstance: any): AnyAction { + const action = { + type: TasksActionTypes.UPDATE_TASK_SUCCESS, + payload: { + taskInstance, + }, + }; + + return action; +} + +function updateTaskFailed(error: any, taskInstance: any): AnyAction { + const action = { + type: TasksActionTypes.UPDATE_TASK_FAILED, + payload: { + error, + taskInstance, + }, + }; + + return action; +} + +export function updateTaskAsync(taskInstance: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + dispatch(updateTask()); + await taskInstance.save(); + const [task] = await cvat.tasks.get({ id: taskInstance.id }); + dispatch(updateTaskSuccess(task)); + } catch (error) { + // try abort all changes + let task = null; + try { + [task] = await cvat.tasks.get({ id: taskInstance.id }); + } catch (fetchError) { + dispatch(updateTaskFailed(error, taskInstance)); + return; + } + + dispatch(updateTaskFailed(error, task)); + } + }; +} + +// a job is a part of a task, so for simplify we consider +// updating the job as updating a task +export function updateJobAsync(jobInstance: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + dispatch(updateTask()); + await jobInstance.save(); + const [task] = await cvat.tasks.get({ id: jobInstance.task.id }); + dispatch(updateTaskSuccess(task)); + } catch (error) { + // try abort all changes + let task = null; + try { + [task] = await cvat.tasks.get({ id: jobInstance.task.id }); + } catch (fetchError) { + dispatch(updateTaskFailed(error, jobInstance.task)); + return; + } + + dispatch(updateTaskFailed(error, task)); + } + }; +} diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index 7ca9805dbb4..c782aaf22a6 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -5,6 +5,7 @@ import { Modal, } from 'antd'; +import Text from 'antd/lib/typography/Text'; import { ClickParam } from 'antd/lib/menu/index'; import LoaderItemComponent from './loader-item'; @@ -18,15 +19,18 @@ interface ActionsMenuComponentProps { loadActivity: string | null; dumpActivities: string[] | null; installedTFAnnotation: boolean; + installedTFSegmentation: boolean; installedAutoAnnotation: boolean; onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void; - onDumpAnnotation: (task: any, dumper: any) => void; - onDeleteTask: (task: any) => void; + onDumpAnnotation: (taskInstance: any, dumper: any) => void; + onDeleteTask: (taskInstance: any) => void; + onOpenRunWindow: (taskInstance: any) => void; } interface MinActionsMenuProps { taskInstance: any; onDeleteTask: (task: any) => void; + onOpenRunWindow: (taskInstance: any) => void; } export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam) { @@ -38,11 +42,8 @@ export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam) case 'tracker': { window.open(`${tracker}`, '_blank') return; - } case 'auto': { - - return; - } case 'tf': { - + } case 'auto_annotation': { + props.onOpenRunWindow(taskInstance); return; } case 'delete': { const taskID = taskInstance.id; @@ -63,12 +64,15 @@ export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam) 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, @@ -77,7 +81,9 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) { onDumpAnnotation: props.onDumpAnnotation, } ))} - + {'Upload annotations'} + }> { props.loaders.map((loader) => LoaderItemComponent({ loader, @@ -87,13 +93,8 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) { })) } - {tracker ? Open bug tracker : null} - { props.installedTFAnnotation ? - Run TF annotation : null - } - { props.installedAutoAnnotation ? - Run auto annotation : null - } + {tracker && Open bug tracker} + {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 e870a36373e..e9e9c36e468 100644 --- a/cvat-ui/src/components/actions-menu/dumper-item.tsx +++ b/cvat-ui/src/components/actions-menu/dumper-item.tsx @@ -31,7 +31,7 @@ export default function DumperItemComponent(props: DumperItemComponentProps) { const pending = !!dumpingWithThisDumper; return ( - + ); diff --git a/cvat-ui/src/components/actions-menu/loader-item.tsx b/cvat-ui/src/components/actions-menu/loader-item.tsx index e7eb7a3311f..f1e4ea46ce2 100644 --- a/cvat-ui/src/components/actions-menu/loader-item.tsx +++ b/cvat-ui/src/components/actions-menu/loader-item.tsx @@ -27,7 +27,7 @@ export default function LoaderItemComponent(props: LoaderItemComponentProps) { const pending = !!loadingWithThisLoader; return ( - + {loader.name} - {pending ? : null} + {pending && } 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 new file mode 100644 index 00000000000..94c36e111cd --- /dev/null +++ b/cvat-ui/src/components/create-model-page/create-model-content.tsx @@ -0,0 +1,139 @@ +import React from 'react'; + +import { + Row, + Col, + Icon, + Alert, + Button, + Tooltip, + Modal, + message, +} from 'antd'; + +import CreateModelForm, { + CreateModelForm as WrappedCreateModelForm +} from './create-model-form'; +import ConnectedFileManager, { + FileManagerContainer +} from '../../containers/file-manager/file-manager'; +import { ModelFiles } from '../../reducers/interfaces'; + +interface Props { + createModel(name: string, files: ModelFiles, global: boolean): void; + isAdmin: boolean; + modelCreatingError: string; + modelCreatingStatus: string; +} + +export default class CreateModelContent extends React.PureComponent { + private modelForm: WrappedCreateModelForm; + private fileManagerContainer: FileManagerContainer; + public constructor(props: Props) { + super(props); + this.modelForm = null as any as WrappedCreateModelForm; + this.fileManagerContainer = null as any as FileManagerContainer; + } + + private handleSubmitClick = () => { + this.modelForm.submit() + .then((data) => { + const { + local, + share, + } = this.fileManagerContainer.getFiles(); + + const files = local.length ? local : share; + const grouppedFiles: ModelFiles = { + xml: '', + bin: '', + py: '', + json: '', + }; + + (files as any).reduce((acc: ModelFiles, value: File | string): ModelFiles => { + const name = typeof value === 'string' ? value : value.name; + const [extension] = name.split('.').reverse(); + if (extension in acc) { + acc[extension] = value; + } + + return acc; + }, grouppedFiles); + + 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', + }); + } else { + this.props.createModel(data.name, grouppedFiles, data.global); + } + }).catch(() => { + Modal.error({ + title: 'Could not upload a model', + content: 'Please, check input fields', + }); + }) + } + + public componentDidUpdate(prevProps: Props) { + if (prevProps.modelCreatingStatus !== 'CREATED' + && this.props.modelCreatingStatus === 'CREATED') { + message.success('The model has been uploaded'); + 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() { + const loading = !!this.props.modelCreatingStatus + && this.props.modelCreatingStatus !== 'CREATED'; + const status = this.props.modelCreatingStatus + && this.props.modelCreatingStatus !== 'CREATED' ? this.props.modelCreatingStatus : ''; + + const guideLink = 'https://github.com/opencv/cvat/tree/develop/cvat/apps/auto_annotation'; + return ( + + + + {window.open(guideLink, '_blank')}} type='question-circle'/> + + + + this.modelForm = ref + } + /> + + + + this.fileManagerContainer = container + }/> + + + {status && } + + + + + + ); + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..44eb7b23cee --- /dev/null +++ b/cvat-ui/src/components/create-model-page/create-model-form.tsx @@ -0,0 +1,85 @@ +import React from 'react'; + +import { + Row, + Col, + Form, + Input, + Tooltip, + Checkbox, +} from 'antd'; + +import { FormComponentProps } from 'antd/lib/form/Form'; +import Text from 'antd/lib/typography/Text'; + +type Props = FormComponentProps; + +export class CreateModelForm extends React.PureComponent { + public constructor(props: Props) { + super(props); + } + + public submit(): Promise<{name: string, global: boolean}> { + return new Promise((resolve, reject) => { + this.props.form.validateFields((errors, values) => { + if (!errors) { + resolve({ + name: values.name, + global: values.global, + }); + } else { + reject(errors); + } + }); + }); + } + + public resetFields() { + this.props.form.resetFields(); + } + + public render() { + const { getFieldDecorator } = this.props.form; + + return ( +
e.preventDefault()}> + + + Name + + + + + + { getFieldDecorator('name', { + rules: [{ + required: true, + message: 'Please, specify a model name', + }], + })()} + + + + + + { getFieldDecorator('global', { + initialValue: false, + valuePropName: 'checked', + })( + + Load globally + + )} + + + + + + + +
+ ); + } +} + +export default Form.create()(CreateModelForm); 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 new file mode 100644 index 00000000000..1f1e2e05a71 --- /dev/null +++ b/cvat-ui/src/components/create-model-page/create-model-page.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { + Row, + Col, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +import CreateModelContent from './create-model-content'; +import { ModelFiles } from '../../reducers/interfaces'; + +interface Props { + createModel(name: string, files: ModelFiles, global: boolean): void; + isAdmin: boolean; + modelCreatingError: string; + modelCreatingStatus: string; +} + +export default function CreateModelPageComponent(props: Props) { + return ( + + + {`Upload a new model`} + + + + ); +} \ No newline at end of file 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 a4a17547cd9..93c100ca442 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 @@ -33,7 +33,7 @@ type Props = FormComponentProps & { }; class AdvancedConfigurationForm extends React.PureComponent { - public async submit() { + public submit() { return new Promise((resolve, reject) => { this.props.form.validateFields((error, values) => { if (!error) { @@ -79,7 +79,7 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( - Image quality + {'Image quality'} {this.props.form.getFieldDecorator('imageQuality', { initialValue: 70, rules: [{ @@ -104,7 +104,7 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( - Overlap size + {'Overlap size'} {this.props.form.getFieldDecorator('overlapSize')( )} @@ -117,7 +117,7 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( - Segment size + {'Segment size'} {this.props.form.getFieldDecorator('segmentSize')( )} @@ -129,7 +129,7 @@ class AdvancedConfigurationForm extends React.PureComponent { private renderStartFrame() { return ( - Start frame + {'Start frame'} {this.props.form.getFieldDecorator('startFrame')( { private renderStopFrame() { return ( - Stop frame + {'Stop frame'} {this.props.form.getFieldDecorator('stopFrame')( { private renderFrameStep() { return ( - Frame step + {'Frame step'} {this.props.form.getFieldDecorator('frameStep')( { - Dataset repository URL + {'Dataset repository URL'} {this.props.form.getFieldDecorator('repository', { - // TODO: Add pattern + rules: [{ + validator: (_, value, callback) => { + const [url, path] = value.split(/\s+/); + if (!patterns.validateURL.pattern.test(url)) { + callback('Git URL is not a valid'); + } + + if (path && !patterns.validatePath.pattern.test(path)) { + callback('Git path is not a valid'); + } + + callback(); + } + }] })( { return ( - Issue tracker + {'Issue tracker'} {this.props.form.getFieldDecorator('bugTracker', { rules: [{ ...patterns.validateURL, 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 5ffa9a4b096..2d0d5d0a656 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 @@ -16,7 +16,7 @@ type Props = FormComponentProps & { }; class BasicConfigurationForm extends React.PureComponent { - public async submit() { + public submit() { return new Promise((resolve, reject) => { this.props.form.validateFields((error, values) => { if (!error) { @@ -39,13 +39,13 @@ class BasicConfigurationForm extends React.PureComponent { const { getFieldDecorator } = this.props.form; return (
e.preventDefault()}> - Name + Name { getFieldDecorator('name', { rules: [{ required: true, message: 'Please, specify a name', - }] // TODO: Add task name pattern + }] })( ) } 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 4274bd9f87d..9b4ac355d87 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -137,7 +137,7 @@ export default class CreateTaskContent extends React.PureComponent private renderLabelsBlock() { return ( - Labels + Labels this.fileManagerContainer = container - }/> + } withRemote={true}/> ); } @@ -169,7 +169,7 @@ export default class CreateTaskContent extends React.PureComponent Advanced configuration + {'Advanced configuration'} } key='1'> return ( - Basic configuration + {'Basic configuration'} { this.renderBasicBlock() } @@ -226,10 +226,10 @@ export default class CreateTaskContent extends React.PureComponent { this.renderFilesBlock() } { this.renderAdvancedBlock() } - + {loading ? : null} - + - { props.installedAutoAnnotation ? - : null - } - { props.installedAnalytics ? + + { renderModels ? + : null + } + { props.installedAnalytics ? + : null + } + +
+ : null - } -
-
- - - - - + () => { + const serverHost = core.config.backendAPI.slice(0, -7); + window.open(`${serverHost}/documentation/user_guide.html`, '_blank') + } + }> Help + + - - {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/label-form.tsx b/cvat-ui/src/components/labels-editor/label-form.tsx index 09e526acd88..f9870c653ab 100644 --- a/cvat-ui/src/components/labels-editor/label-form.tsx +++ b/cvat-ui/src/components/labels-editor/label-form.tsx @@ -439,7 +439,7 @@ class LabelForm extends React.PureComponent { { attributeItems.length > 0 ? - Attributes + Attributes : null } diff --git a/cvat-ui/src/components/labels-editor/labels-editor.tsx b/cvat-ui/src/components/labels-editor/labels-editor.tsx index 77392bd71b1..6c7f24a1878 100644 --- a/cvat-ui/src/components/labels-editor/labels-editor.tsx +++ b/cvat-ui/src/components/labels-editor/labels-editor.tsx @@ -204,7 +204,7 @@ export default class LabelsEditor - Raw + Raw } key='1'> - Constructor + Constructor } key='2'> { 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 new file mode 100644 index 00000000000..aeb0439306b --- /dev/null +++ b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx @@ -0,0 +1,340 @@ +import React from 'react'; + +import { + Row, + Col, + Tag, + Spin, + Icon, + Modal, + Select, + Tooltip, + Checkbox, +} from 'antd'; + +import { Model } from '../../reducers/interfaces'; + +interface Props { + modelsInitialized: boolean; + models: Model[]; + activeProcesses: { + [index: string]: string; + }; + visible: boolean; + taskInstance: any; + startingError: string; + getModels(): void; + closeDialog(): void; + runInference( + taskInstance: any, + model: Model, + mapping: { + [index: string]: string + }, + cleanOut: boolean, + ): void; +} + +interface State { + selectedModel: string | null; + cleanOut: boolean; + mapping: { + [index: string]: string; + }; + colors: { + [index: string]: string; + }; + matching: { + model: string, + task: string, + }; +} + +const nextColor = (function *() { + const values = [ + 'magenta', 'green', 'geekblue', + 'orange', 'red', 'cyan', + 'blue', 'volcano', 'purple', + ]; + + for (let i = 0; i < values.length; i++) { + yield values[i]; + if (i === values.length) { + i = 0; + } + } +})(); + +export default class ModelRunnerModalComponent extends React.PureComponent { + public constructor(props: Props) { + super(props); + this.state = { + selectedModel: null, + mapping: {}, + colors: {}, + cleanOut: false, + matching: { + model: '', + task: '', + }, + }; + } + + private renderModelSelector() { + return ( + + Model: + + + + + ); + } + + private renderMappingTag(modelLabel: string, taskLabel: string) { + return ( + + + {modelLabel} + + + {taskLabel} + + + + { + const mapping = {...this.state.mapping}; + delete mapping[modelLabel]; + this.setState({ + mapping, + }); + }}/> + + + + ); + } + + private renderMappingInputSelector( + value: string, + current: string, + options: string[] + ) { + return ( + + ); + } + + private renderMappingInput(availableModelLabels: string[], availableTaskLabels: string[]) { + return ( + + + {this.renderMappingInputSelector( + this.state.matching.model, + 'Model', + availableModelLabels, + )} + + + {this.renderMappingInputSelector( + this.state.matching.task, + 'Task', + availableTaskLabels, + )} + + + + + + + + ); + } + + private renderContent() { + const model = this.state.selectedModel && this.props.models + .filter((model) => model.name === this.state.selectedModel)[0]; + + const excludedLabels: { + model: string[], + task: string[], + } = { + model: [], + task: [], + }; + + const withMapping = model && !model.primary; + const tags = withMapping ? Object.keys(this.state.mapping) + .map((modelLabel: string) => { + const taskLabel = this.state.mapping[modelLabel]; + excludedLabels.model.push(modelLabel); + excludedLabels.task.push(taskLabel); + return this.renderMappingTag( + modelLabel, + this.state.mapping[modelLabel], + ); + }) : []; + + const availableModelLabels = model ? model.labels + .filter( + (label: string) => !excludedLabels.model.includes(label), + ) : []; + const availableTaskLabels = this.props.taskInstance.labels + .map( + (label: any) => label.name, + ).filter((label: string) => !excludedLabels.task.includes(label)) + + const mappingISAvailable = !!availableModelLabels.length + && !!availableTaskLabels.length; + + return ( +
+ { this.renderModelSelector() } + { withMapping && tags} + { withMapping + && mappingISAvailable + && this.renderMappingInput(availableModelLabels, availableTaskLabels) + } + { withMapping && +
+ this.setState({ + cleanOut: e.target.checked, + })} + > Clean old annotations +
+ } +
+ ); + } + + private renderSpin() { + return ( + + ); + } + + public componentDidUpdate(prevProps: Props) { + if (!prevProps.visible && this.props.visible) { + this.setState({ + selectedModel: null, + mapping: {}, + matching: { + model: '', + task: '', + }, + cleanOut: false, + }); + } + + if (!prevProps.startingError && this.props.startingError) { + Modal.error({ + title: 'Could not start model inference', + content: this.props.startingError, + }); + } + } + + public componentDidMount() { + if (!this.props.modelsInitialized) { + this.props.getModels(); + } + } + + public render() { + const activeModel = this.props.models.filter( + (model) => model.name === this.state.selectedModel + )[0]; + + let enabledSubmit = !!activeModel + && activeModel.primary || !!Object.keys(this.state.mapping).length; + + return ( + this.props.visible && { + this.props.runInference( + this.props.taskInstance, + this.props.models + .filter((model) => model.name === this.state.selectedModel)[0], + this.state.mapping, + this.state.cleanOut, + ); + this.props.closeDialog() + }} + onCancel={() => this.props.closeDialog()} + okButtonProps={{disabled: !enabledSubmit}} + title='Automatic annotation' + visible={true} + > + {!this.props.modelsInitialized && this.renderSpin()} + {this.props.modelsInitialized && this.renderContent()} + + ); + } +} \ No newline at end of file diff --git a/cvat-ui/src/components/models-page/built-model-item.tsx b/cvat-ui/src/components/models-page/built-model-item.tsx new file mode 100644 index 00000000000..0bfd1bb7150 --- /dev/null +++ b/cvat-ui/src/components/models-page/built-model-item.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { + Row, + Col, + Tag, + Select, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +import { Model } from '../../reducers/interfaces'; + +interface Props { + model: Model; +} + +export default function BuiltModelItemComponent(props: Props) { + return ( + + + Tensorflow + + + + {props.model.name} + + + + + + + + ); +} diff --git a/cvat-ui/src/components/models-page/built-models-list.tsx b/cvat-ui/src/components/models-page/built-models-list.tsx new file mode 100644 index 00000000000..50e76de5811 --- /dev/null +++ b/cvat-ui/src/components/models-page/built-models-list.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import { + Row, + Col, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +import BuiltModelItemComponent from './built-model-item'; +import { Model } from '../../reducers/interfaces'; + +interface Props { + models: Model[]; +} + +export default function IntegratedModelsListComponent(props: Props) { + const items = props.models.map((model) => + + ); + + return ( + <> + + + Primary + + + + + + + {'Framework'} + + + {'Name'} + + + Labels + + + { items } + + + + ); +} diff --git a/cvat-ui/src/components/models-page/empty-list.tsx b/cvat-ui/src/components/models-page/empty-list.tsx new file mode 100644 index 00000000000..1746eaa8471 --- /dev/null +++ b/cvat-ui/src/components/models-page/empty-list.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { Link } from 'react-router-dom'; +import Text from 'antd/lib/typography/Text'; +import { + Col, + Row, + Icon, +} from 'antd'; + +export default function EmptyListComponent() { + const emptyTasksIcon = () => (); + + return ( +
+ + + + + + + + {'No models uploaded yet ...'} + + + + + {'To annotate your tasks automatically'} + + + + + {'upload a new model'} + + +
+ + ) +} \ No newline at end of file diff --git a/cvat-ui/src/components/models-page/models-page.tsx b/cvat-ui/src/components/models-page/models-page.tsx new file mode 100644 index 00000000000..f6d3df5099a --- /dev/null +++ b/cvat-ui/src/components/models-page/models-page.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import { + Spin, +} from 'antd'; + +import TopBarComponent from './top-bar'; +import UploadedModelsList from './uploaded-models-list'; +import BuiltModelsList from './built-models-list'; +import EmptyListComponent from './empty-list'; +import { Model } from '../../reducers/interfaces'; + +interface Props { + installedAutoAnnotation: boolean; + installedTFSegmentation: boolean; + installedTFAnnotation: boolean; + modelsAreBeingFetched: boolean; + modelsFetchingError: any; + registeredUsers: any[]; + models: Model[]; + getModels(): void; + deleteModel(id: number): void; +} + +export default function ModelsPageComponent(props: Props) { + if (props.modelsAreBeingFetched) { + props.getModels(); + return ( + + ); + } else { + const uploadedModels = props.models.filter((model) => model.id !== null); + const integratedModels = props.models.filter((model) => model.id === null); + + return ( +
+ + { integratedModels.length ? + : null } + { uploadedModels.length && + + } { props.installedAutoAnnotation && + !props.installedTFAnnotation && + !props.installedTFSegmentation && + + } +
+ ); + } +} diff --git a/cvat-ui/src/components/models-page/top-bar.tsx b/cvat-ui/src/components/models-page/top-bar.tsx new file mode 100644 index 00000000000..2b394cd24c7 --- /dev/null +++ b/cvat-ui/src/components/models-page/top-bar.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router'; +import { withRouter } from 'react-router-dom'; + +import { + Col, + Row, + Button, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +type Props = { + installedAutoAnnotation: boolean; +} & RouteComponentProps; + +function TopBarComponent(props: Props) { + return ( + + + Models + + + { props.installedAutoAnnotation && + + } + + + ) +} + +export default withRouter(TopBarComponent); diff --git a/cvat-ui/src/components/models-page/uploaded-model-item.tsx b/cvat-ui/src/components/models-page/uploaded-model-item.tsx new file mode 100644 index 00000000000..01d48897315 --- /dev/null +++ b/cvat-ui/src/components/models-page/uploaded-model-item.tsx @@ -0,0 +1,76 @@ +import React from 'react'; + +import { + Row, + Col, + Tag, + Select, + Menu, + Dropdown, + Button, + Icon, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; +import moment from 'moment'; + +import { Model } from '../../reducers/interfaces'; + +interface Props { + model: Model; + owner: any; + onDelete(): void; +} + +export default function UploadedModelItem(props: Props) { + const subMenuIcon = () => (); + + return ( + + + OpenVINO + + + + {props.model.name} + + + + + {props.owner ? props.owner.username : 'undefined'} + + + + + {moment(props.model.uploadDate).format('MMMM Do YYYY')} + + + + + + + Actions + + { + props.onDelete(); + }}key='delete'>Delete + + }> + + + + + ); +} diff --git a/cvat-ui/src/components/models-page/uploaded-models-list.tsx b/cvat-ui/src/components/models-page/uploaded-models-list.tsx new file mode 100644 index 00000000000..6cc6a1391fb --- /dev/null +++ b/cvat-ui/src/components/models-page/uploaded-models-list.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import { + Row, + Col, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +import UploadedModelItem from './uploaded-model-item'; +import { Model } from '../../reducers/interfaces'; + +interface Props { + registeredUsers: any[]; + models: Model[]; + deleteModel(id: number): void; +} + +export default function UploadedModelsListComponent(props: Props) { + const items = props.models.map((model) => { + const owner = props.registeredUsers.filter((user) => user.id === model.ownerID)[0]; + return ( + props.deleteModel(model.id as number)} + /> + ); + }); + + return ( + <> + + + {'Uploaded by a user'} + + + + + + + {'Framework'} + + + {'Name'} + + + Owner + + + Uploaded + + + Labels + + + + { items } + + + + ); +} diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index 2b9b714cfa1..11cccc2b7fb 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { Row, Col, + Tag, + Icon, Modal, Button, } from 'antd'; @@ -16,6 +18,7 @@ import UserSelector from './user-selector'; import LabelsEditorComponent from '../labels-editor/labels-editor'; import getCore from '../../core'; import patterns from '../../utils/validation-patterns'; +import { getReposData, syncRepos } from '../../utils/git-utils'; const core = getCore(); @@ -30,17 +33,24 @@ interface Props { interface State { name: string; bugTracker: string; + repository: string; + repositoryStatus: string; } export default class DetailsComponent extends React.PureComponent { + private mounted: boolean; + constructor(props: Props) { super(props); const { taskInstance } = props; + this.mounted = false; this.state = { name: taskInstance.name, bugTracker: taskInstance.bugTracker, + repository: '', + repositoryStatus: '', }; } @@ -84,24 +94,24 @@ export default class DetailsComponent extends React.PureComponent <> - Overlap size + {'Overlap size'}
{overlap} - Segment size + {'Segment size'}
{segmentSize}
- Image quality + {'Image quality'}
{imageQuality} - Z-order + {'Z-order'}
{zOrder} @@ -150,6 +160,51 @@ export default class DetailsComponent extends React.PureComponent ); } + private renderDatasetRepository() { + const { repository } = this.state; + const { repositoryStatus } = this.state; + + return ( + repository ? + + + {'Dataset Repository'} +
+ {repository} + {repositoryStatus === 'sync' ? + + Synchronized + : repositoryStatus === 'merged' ? + + Merged + : repositoryStatus === 'syncing' ? + + Syncing : + { + this.setState({ + repositoryStatus: 'syncing', + }); + + syncRepos(this.props.taskInstance.id).then(() => { + if (this.mounted) { + this.setState({ + repositoryStatus: 'sync', + }); + } + }).catch(() => { + if (this.mounted) { + this.setState({ + repositoryStatus: '!sync', + }); + } + }); + }}> Synchronize + } + +
: null + ); + } + private renderBugTracker() { const { taskInstance } = this.props; const { bugTracker } = this.state; @@ -174,7 +229,7 @@ export default class DetailsComponent extends React.PureComponent return ( - Issue Tracker + {'Issue Tracker'}
{bugTracker} diff --git a/cvat-ui/src/components/tasks-page/empty-list.tsx b/cvat-ui/src/components/tasks-page/empty-list.tsx index ef4b2e2255a..8b631631142 100644 --- a/cvat-ui/src/components/tasks-page/empty-list.tsx +++ b/cvat-ui/src/components/tasks-page/empty-list.tsx @@ -20,17 +20,17 @@ export default function EmptyListComponent() {
- No tasks created yet ... + {'No tasks created yet ...'} - To get started with your annotation project + {'To get started with your annotation project'} - create new task + {'create a new task'} diff --git a/cvat-ui/src/components/tasks-page/task-item.tsx b/cvat-ui/src/components/tasks-page/task-item.tsx index 8faa9e2c8a3..658a2a700a8 100644 --- a/cvat-ui/src/components/tasks-page/task-item.tsx +++ b/cvat-ui/src/components/tasks-page/task-item.tsx @@ -14,21 +14,12 @@ import { import moment from 'moment'; -import ActionsMenu from '../actions-menu/actions-menu'; +import ActionsMenuContainer from '../../containers/actions-menu/actions-menu'; export interface TaskItemProps { - installedTFAnnotation: boolean; - installedAutoAnnotation: boolean; taskInstance: any; previewImage: string; - dumpActivities: string[] | null; - loadActivity: string | null; - loaders: any[]; - dumpers: any[]; deleted: boolean; - onDeleteTask: (taskInstance: any) => void; - onDumpAnnotation: (task: any, dumper: any) => void; - onLoadAnnotation: (task: any, loader: any, file: File) => void; } class TaskItemComponent extends React.PureComponent { @@ -47,7 +38,7 @@ class TaskItemComponent extends React.PureComponent
: null } - Last updated {updated} + {`Last updated ${updated}`} ) } @@ -92,10 +83,10 @@ class TaskItemComponent extends React.PureComponent { numOfCompleted === numOfJobs ? - Completed + {'Completed'} : numOfCompleted ? - In Progress - : Pending + {'In Progress'} + : {'Pending'} } @@ -131,20 +122,11 @@ class TaskItemComponent extends React.PureComponent - Actions + Actions }> diff --git a/cvat-ui/src/components/tasks-page/tasks-page.tsx b/cvat-ui/src/components/tasks-page/tasks-page.tsx index 7043e296feb..424620cab20 100644 --- a/cvat-ui/src/components/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/components/tasks-page/tasks-page.tsx @@ -124,6 +124,12 @@ class TasksPageComponent extends React.PureComponent { - public render() { - return ( - <> - - - Default project - - - - - Tasks - - - - - - - - ) - } +function TopBarComponent(props: VisibleTopBarProps & RouteComponentProps) { + return ( + <> + + + {'Default project'} + + + + + Tasks + + + + + + + + ) } -export default withRouter(TopBarComponent); \ No newline at end of file +export default withRouter(TopBarComponent); diff --git a/cvat-ui/src/containers/actions-menu/actions-menu.tsx b/cvat-ui/src/containers/actions-menu/actions-menu.tsx new file mode 100644 index 00000000000..066dc364064 --- /dev/null +++ b/cvat-ui/src/containers/actions-menu/actions-menu.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import ActionsMenuComponent from '../../components/actions-menu/actions-menu'; +import { CombinedState } from '../../reducers/interfaces'; +import { showRunModelDialog } from '../../actions/models-actions'; +import { + dumpAnnotationsAsync, + loadAnnotationsAsync, + deleteTaskAsync, +} from '../../actions/tasks-actions'; + +interface OwnProps { + taskInstance: any; +} + +interface StateToProps { + loaders: any[]; + dumpers: any[]; + loadActivity: string | null; + dumpActivities: string[] | null; + installedTFAnnotation: boolean; + installedTFSegmentation: boolean; + installedAutoAnnotation: boolean; +}; + +interface DispatchToProps { + onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void; + onDumpAnnotation: (taskInstance: any, dumper: 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 { plugins } = state.plugins; + const id = own.taskInstance.id; + + return { + installedTFAnnotation: plugins.TF_ANNOTATION, + installedTFSegmentation: plugins.TF_SEGMENTATION, + installedAutoAnnotation: plugins.AUTO_ANNOTATION, + dumpActivities: dumps.byTask[id] ? dumps.byTask[id] : null, + loadActivity: loads.byTask[id] ? loads.byTask[id] : null, + loaders: formats.loaders, + dumpers: formats.dumpers, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + onLoadAnnotation: (taskInstance: any, loader: any, file: File) => { + dispatch(loadAnnotationsAsync(taskInstance, loader, file)); + }, + onDumpAnnotation: (taskInstance: any, dumper: any) => { + dispatch(dumpAnnotationsAsync(taskInstance, dumper)); + }, + onDeleteTask: (taskInstance: any) => { + dispatch(deleteTaskAsync(taskInstance)); + }, + onOpenRunWindow: (taskInstance: any) => { + dispatch(showRunModelDialog(taskInstance)); + } + }; +} + +function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps) { + return ( + + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ActionsMenuContainer); 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 new file mode 100644 index 00000000000..1e47db0d60b --- /dev/null +++ b/cvat-ui/src/containers/create-model-page/create-model-page.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import CreateModelPageComponent from '../../components/create-model-page/create-model-page'; +import { createModelAsync } from '../../actions/models-actions'; +import { + ModelFiles, + CombinedState, +} from '../../reducers/interfaces'; + +interface StateToProps { + isAdmin: boolean; + modelCreatingError: any; + modelCreatingStatus: string; +} + +interface DispatchToProps { + createModel(name: string, files: ModelFiles, global: boolean): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { models} = state; + + return { + isAdmin: state.auth.user.isAdmin, + modelCreatingError: models.creatingError, + modelCreatingStatus: models.creatingStatus, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + createModel(name: string, files: ModelFiles, global: boolean) { + dispatch(createModelAsync(name, files, global)); + }, + }; +} + +function CreateModelPageContainer(props: StateToProps & DispatchToProps) { + return ( + + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(CreateModelPageContainer); 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 2d4b15a63a4..2deb946116d 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 @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { CombinedState } from '../../reducers/root-reducer'; +import { CombinedState } from '../../reducers/interfaces'; import CreateTaskComponent from '../../components/create-task-page/create-task-page'; import { CreateTaskData } from '../../components/create-task-page/create-task-content'; import { createTaskAsync } from '../../actions/tasks-actions'; diff --git a/cvat-ui/src/containers/file-manager/file-manager.tsx b/cvat-ui/src/containers/file-manager/file-manager.tsx index 108aef746a5..1e89203be51 100644 --- a/cvat-ui/src/containers/file-manager/file-manager.tsx +++ b/cvat-ui/src/containers/file-manager/file-manager.tsx @@ -5,8 +5,14 @@ import { TreeNodeNormal } from 'antd/lib/tree/Tree' import FileManagerComponent, { Files } from '../../components/file-manager/file-manager'; import { loadShareDataAsync } from '../../actions/share-actions'; -import { ShareItem } from '../../reducers/interfaces'; -import { CombinedState } from '../../reducers/root-reducer'; +import { + ShareItem, + CombinedState, +} from '../../reducers/interfaces'; + +interface OwnProps { + withRemote: boolean; +} interface StateToProps { treeData: TreeNodeNormal[]; @@ -43,7 +49,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { }; } -class FileManagerContainer extends React.PureComponent { +type Props = StateToProps & DispatchToProps & OwnProps; + +export class FileManagerContainer extends React.PureComponent { private managerComponentRef: any; public getFiles(): Files { @@ -59,6 +67,7 @@ class FileManagerContainer extends React.PureComponent this.managerComponentRef = component} /> ); diff --git a/cvat-ui/src/containers/header/header.tsx b/cvat-ui/src/containers/header/header.tsx index 41c1ff361a2..cb8b266c813 100644 --- a/cvat-ui/src/containers/header/header.tsx +++ b/cvat-ui/src/containers/header/header.tsx @@ -2,14 +2,18 @@ import React from 'react'; import { connect } from 'react-redux'; import { logoutAsync } from '../../actions/auth-actions'; -import { CombinedState } from '../../reducers/root-reducer'; -import { SupportedPlugins } from '../../reducers/interfaces'; +import { + SupportedPlugins, + CombinedState, +} from '../../reducers/interfaces'; import HeaderComponent from '../../components/header/header'; interface StateToProps { installedAnalytics: boolean; installedAutoAnnotation: boolean; + installedTFSegmentation: boolean; + installedTFAnnotation: boolean; username: string; logoutError: any; } @@ -24,6 +28,8 @@ function mapStateToProps(state: CombinedState): StateToProps { return { installedAnalytics: plugins[SupportedPlugins.ANALYTICS], installedAutoAnnotation: plugins[SupportedPlugins.AUTO_ANNOTATION], + installedTFSegmentation: plugins[SupportedPlugins.TF_SEGMENTATION], + installedTFAnnotation: plugins[SupportedPlugins.TF_ANNOTATION], username: auth.user.username, logoutError: auth.logoutError, }; @@ -39,6 +45,8 @@ function HeaderContainer(props: StateToProps & DispatchToProps) { return ( + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +) (ModelRunnerModalContainer); diff --git a/cvat-ui/src/containers/models-page/empty-page.tsx b/cvat-ui/src/containers/models-page/empty-page.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/cvat-ui/src/containers/models-page/models-page.tsx b/cvat-ui/src/containers/models-page/models-page.tsx index a69b53431ed..e10cff23ca2 100644 --- a/cvat-ui/src/containers/models-page/models-page.tsx +++ b/cvat-ui/src/containers/models-page/models-page.tsx @@ -1,13 +1,79 @@ import React from 'react'; +import { connect } from 'react-redux'; -export default class ModelsPageContainer extends React.PureComponent { - constructor(props: any) { - super(props); - } - - public render() { - return ( -
Models page
- ) - } -} \ No newline at end of file +import ModelsPageComponent from '../../components/models-page/models-page'; +import { + Model, + CombinedState, +} from '../../reducers/interfaces'; +import { + getModelsAsync, + deleteModelAsync, +} from '../../actions/models-actions'; + +interface StateToProps { + installedAutoAnnotation: boolean; + installedTFAnnotation: boolean; + installedTFSegmentation: boolean; + modelsAreBeingFetched: boolean; + modelsFetchingError: any; + models: Model[]; + registeredUsers: any[]; +} + +interface DispatchToProps { + getModels(): void; + deleteModel(id: number): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { plugins } = state.plugins; + const { models } = state; + + return { + installedAutoAnnotation: plugins.AUTO_ANNOTATION, + installedTFAnnotation: plugins.TF_ANNOTATION, + installedTFSegmentation: plugins.TF_SEGMENTATION, + modelsAreBeingFetched: !models.initialized, + modelsFetchingError: models.fetchingError, + models: models.models, + registeredUsers: state.users.users, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + getModels() { + dispatch(getModelsAsync()); + }, + deleteModel(id: number) { + dispatch(deleteModelAsync(id)); + }, + }; +} + +function ModelsPageContainer(props: DispatchToProps & StateToProps) { + const render = props.installedAutoAnnotation + || props.installedTFAnnotation + || props.installedTFSegmentation; + + return ( + render ? + : null + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ModelsPageContainer); diff --git a/cvat-ui/src/containers/register-page/register-page.tsx b/cvat-ui/src/containers/register-page/register-page.tsx index 6aa4e79d56c..bdd7f230ac3 100644 --- a/cvat-ui/src/containers/register-page/register-page.tsx +++ b/cvat-ui/src/containers/register-page/register-page.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { registerAsync } from '../../actions/auth-actions'; -import { CombinedState } from '../../reducers/root-reducer'; +import { CombinedState } from '../../reducers/interfaces'; import RegisterPageComponent from '../../components/register-page/register-page'; interface StateToProps { diff --git a/cvat-ui/src/containers/task-page/details.tsx b/cvat-ui/src/containers/task-page/details.tsx index 615de83d8e7..1f5155f34d6 100644 --- a/cvat-ui/src/containers/task-page/details.tsx +++ b/cvat-ui/src/containers/task-page/details.tsx @@ -2,12 +2,17 @@ import React from 'react'; import { connect } from 'react-redux'; import DetailsComponent from '../../components/task-page/details'; -import { CombinedState } from '../../reducers/root-reducer'; -import { updateTaskAsync } from '../../actions/task-actions'; +import { updateTaskAsync } from '../../actions/tasks-actions'; +import { + Task, + CombinedState, +} from '../../reducers/interfaces'; + +interface OwnProps { + task: Task; +} interface StateToProps { - previewImage: string; - taskInstance: any; registeredUsers: any[]; installedGit: boolean; } @@ -16,15 +21,11 @@ interface DispatchToProps { onTaskUpdate: (taskInstance: any) => void; } -function mapStateToProps(state: CombinedState): StateToProps { +function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { const { plugins } = state.plugins; - const taskInstance = (state.activeTask.task as any).instance; - const previewImage = (state.activeTask.task as any).preview; return { registeredUsers: state.users.users, - taskInstance, - previewImage, installedGit: plugins.GIT_INTEGRATION, }; } @@ -38,11 +39,11 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { } -function TaskPageContainer(props: StateToProps & DispatchToProps) { +function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps) { return ( diff --git a/cvat-ui/src/containers/task-page/task-page.tsx b/cvat-ui/src/containers/task-page/task-page.tsx index 5bea0169faa..bc29944ff4d 100644 --- a/cvat-ui/src/containers/task-page/task-page.tsx +++ b/cvat-ui/src/containers/task-page/task-page.tsx @@ -1,15 +1,23 @@ import React from 'react'; import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import { RouteComponentProps } from 'react-router'; -import { getTaskAsync } from '../../actions/task-actions'; +import { getTasksAsync } from '../../actions/tasks-actions'; import TaskPageComponent from '../../components/task-page/task-page'; -import { CombinedState } from '../../reducers/root-reducer'; +import { + Task, + CombinedState, +} from '../../reducers/interfaces'; + +type Props = RouteComponentProps<{id: string}>; interface StateToProps { + task: Task; taskFetchingError: any; taskUpdatingError: any; - taskInstance: any; + taskDeletingError: any; deleteActivity: boolean | null; installedGit: boolean; } @@ -18,23 +26,25 @@ interface DispatchToProps { fetchTask: (tid: number) => void; } -function mapStateToProps(state: CombinedState): StateToProps { +function mapStateToProps(state: CombinedState, own: Props): StateToProps { const { plugins } = state.plugins; - const { activeTask } = state; const { deletes } = state.tasks.activities; + const taskDeletingError = deletes.deletingError; + const id = +own.match.params.id; - const taskInstance = activeTask.task ? activeTask.task.instance : null; + const filtered = state.tasks.current.filter((task) => task.instance.id === id); + const task = filtered[0] || null; let deleteActivity = null; - if (taskInstance) { - const { id } = taskInstance; - deleteActivity = deletes.byTask[id] ? deletes.byTask[id] : null; + if (task && id in deletes.byTask) { + deleteActivity = deletes.byTask[id]; } return { - taskInstance, - taskFetchingError: activeTask.taskFetchingError, - taskUpdatingError: activeTask.taskUpdatingError, + task, + taskFetchingError: state.tasks.tasksFetchingError, + taskUpdatingError: state.tasks.taskUpdatingError, + taskDeletingError, deleteActivity, installedGit: plugins.GIT_INTEGRATION, }; @@ -43,7 +53,16 @@ function mapStateToProps(state: CombinedState): StateToProps { function mapDispatchToProps(dispatch: any): DispatchToProps { return { fetchTask: (tid: number) => { - dispatch(getTaskAsync(tid)); + dispatch(getTasksAsync({ + id: tid, + page: 1, + search: null, + owner: null, + assignee: null, + name: null, + status: null, + mode: null, + })); }, }; } @@ -51,7 +70,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { function TaskPageContainer(props: StateToProps & DispatchToProps) { return ( void; - dumpAnnotations: (task: any, format: string) => void; - loadAnnotations: (task: any, format: string, file: File) => void; -} - -function mapStateToProps(state: CombinedState): StateToProps { - const taskInstance = (state.activeTask.task as any).instance; - - const { plugins } = state.plugins; - const { formats } = state; - const { dumps } = state.tasks.activities; - const { loads } = state.tasks.activities; - - const { id } = taskInstance; - const dumpActivities = dumps.byTask[id] ? dumps.byTask[id] : null; - const loadActivity = loads.byTask[id] ? loads.byTask[id] : null; - - return { - taskInstance, - loaders: formats.loaders, - dumpers: formats.dumpers, - dumpActivities, - loadActivity, - installedTFAnnotation: plugins.TF_ANNOTATION, - installedAutoAnnotation: plugins.AUTO_ANNOTATION, - }; -} - -function mapDispatchToProps(dispatch: any): DispatchToProps { - return { - deleteTask: (taskInstance: any): void => { - dispatch(deleteTaskAsync(taskInstance)); - }, - dumpAnnotations: (task: any, dumper: any): void => { - dispatch(dumpAnnotationsAsync(task, dumper)); - }, - loadAnnotations: (task: any, loader: any, file: File): void => { - dispatch(loadAnnotationsAsync(task, loader, file)); - }, - }; -} - -function TaskPageContainer(props: StateToProps & DispatchToProps) { - return ( - - ); -} - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(TaskPageContainer); \ No newline at end of file diff --git a/cvat-ui/src/containers/tasks-page/task-item.tsx b/cvat-ui/src/containers/tasks-page/task-item.tsx index 2f14010f222..b01159750db 100644 --- a/cvat-ui/src/containers/tasks-page/task-item.tsx +++ b/cvat-ui/src/containers/tasks-page/task-item.tsx @@ -3,39 +3,23 @@ import { connect } from 'react-redux'; import { TasksQuery, - SupportedPlugins, -} from '../../reducers/interfaces'; - -import { CombinedState, -} from '../../reducers/root-reducer'; +} from '../../reducers/interfaces'; import TaskItemComponent from '../../components/tasks-page/task-item' import { getTasksAsync, - dumpAnnotationsAsync, - loadAnnotationsAsync, - deleteTaskAsync, } from '../../actions/tasks-actions'; interface StateToProps { - installedTFAnnotation: boolean; - installedAutoAnnotation: boolean; - dumpActivities: string[] | null; - loadActivity: string | null; deleteActivity: boolean | null; previewImage: string; taskInstance: any; - loaders: any[]; - dumpers: any[]; } interface DispatchToProps { getTasks: (query: TasksQuery) => void; - delete: (taskInstance: any) => void; - dump: (task: any, format: string) => void; - load: (task: any, format: string, file: File) => void; } interface OwnProps { @@ -45,23 +29,13 @@ interface OwnProps { function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { const task = state.tasks.current[own.idx]; - const { formats } = state; - const { dumps } = state.tasks.activities; - const { loads } = state.tasks.activities; const { deletes } = state.tasks.activities; - const { plugins } = state.plugins; const id = own.taskID; return { - installedTFAnnotation: plugins.TF_ANNOTATION, - installedAutoAnnotation: plugins.AUTO_ANNOTATION, - dumpActivities: dumps.byTask[id] ? dumps.byTask[id] : null, - loadActivity: loads.byTask[id] ? loads.byTask[id] : null, deleteActivity: deletes.byTask[id] ? deletes.byTask[id] : null, previewImage: task.preview, taskInstance: task.instance, - loaders: formats.loaders, - dumpers: formats.dumpers, }; } @@ -70,15 +44,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { getTasks: (query: TasksQuery): void => { dispatch(getTasksAsync(query)); }, - dump: (task: any, dumper: any): void => { - dispatch(dumpAnnotationsAsync(task, dumper)); - }, - load: (task: any, loader: any, file: File): void => { - dispatch(loadAnnotationsAsync(task, loader, file)); - }, - delete: (taskInstance: any): void => { - dispatch(deleteTaskAsync(taskInstance)); - }, } } @@ -87,18 +52,9 @@ type TasksItemContainerProps = StateToProps & DispatchToProps & OwnProps; function TaskItemContainer(props: TasksItemContainerProps) { return ( ); } diff --git a/cvat-ui/src/containers/tasks-page/tasks-list.tsx b/cvat-ui/src/containers/tasks-page/tasks-list.tsx index 0b689865485..8782e3f9fb1 100644 --- a/cvat-ui/src/containers/tasks-page/tasks-list.tsx +++ b/cvat-ui/src/containers/tasks-page/tasks-list.tsx @@ -4,11 +4,8 @@ import { connect } from 'react-redux'; import { TasksState, TasksQuery, -} from '../../reducers/interfaces'; - -import { CombinedState, -} from '../../reducers/root-reducer'; +} from '../../reducers/interfaces'; import TasksListComponent from '../../components/tasks-page/task-list'; diff --git a/cvat-ui/src/containers/tasks-page/tasks-page.tsx b/cvat-ui/src/containers/tasks-page/tasks-page.tsx index a6fcf538f5a..1ad0d54c8e2 100644 --- a/cvat-ui/src/containers/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/containers/tasks-page/tasks-page.tsx @@ -3,8 +3,8 @@ import { connect } from 'react-redux'; import { TasksQuery, + CombinedState } from '../../reducers/interfaces'; -import { CombinedState } from '../../reducers/root-reducer'; import TasksPageComponent from '../../components/tasks-page/tasks-page'; diff --git a/cvat-ui/src/index.tsx b/cvat-ui/src/index.tsx index f8c68d42748..d090f45b45c 100644 --- a/cvat-ui/src/index.tsx +++ b/cvat-ui/src/index.tsx @@ -3,16 +3,19 @@ import ReactDOM from 'react-dom'; import { connect, Provider } from 'react-redux'; import CVATApplication from './components/cvat-app'; -import createCVATStore from './store'; + +import createRootReducer from './reducers/root-reducer'; +import createCVATStore, { getCVATStore } from './store'; import { authorizedAsync } from './actions/auth-actions'; import { gettingFormatsAsync } from './actions/formats-actions'; import { checkPluginsAsync } from './actions/plugins-actions'; import { getUsersAsync } from './actions/users-actions'; -import { CombinedState } from './reducers/root-reducer'; +import { CombinedState } from './reducers/interfaces'; -const cvatStore = createCVATStore(); +createCVATStore(createRootReducer); +const cvatStore = getCVATStore(); interface StateToProps { pluginsInitialized: boolean; @@ -22,6 +25,9 @@ interface StateToProps { gettingAuthError: any; gettingFormatsError: any; gettingUsersError: any; + installedAutoAnnotation: boolean; + installedTFSegmentation: boolean; + installedTFAnnotation: boolean; user: any; } @@ -46,6 +52,9 @@ function mapStateToProps(state: CombinedState): StateToProps { gettingAuthError: auth.authError, gettingUsersError: users.gettingUsersError, gettingFormatsError: formats.gettingFormatsError, + installedAutoAnnotation: plugins.plugins.AUTO_ANNOTATION, + installedTFSegmentation: plugins.plugins.TF_SEGMENTATION, + installedTFAnnotation: plugins.plugins.TF_ANNOTATION, user: auth.user, }; } @@ -73,6 +82,9 @@ function reduxAppWrapper(props: StateToProps & DispatchToProps) { gettingAuthError={props.gettingAuthError ? props.gettingAuthError.toString() : ''} gettingFormatsError={props.gettingFormatsError ? props.gettingFormatsError.toString() : ''} gettingUsersError={props.gettingUsersError ? props.gettingUsersError.toString() : ''} + installedAutoAnnotation={props.installedAutoAnnotation} + installedTFSegmentation={props.installedTFSegmentation} + installedTFAnnotation={props.installedTFAnnotation} user={props.user} /> ) diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 688acb17cef..bd51b400da6 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -27,6 +27,7 @@ export interface Task { export interface TasksState { initialized: boolean; tasksFetchingError: any; + taskUpdatingError: any; gettingQuery: TasksQuery; count: number; current: Task[]; @@ -71,6 +72,7 @@ export enum SupportedPlugins { GIT_INTEGRATION = 'GIT_INTEGRATION', AUTO_ANNOTATION = 'AUTO_ANNOTATION', TF_ANNOTATION = 'TF_ANNOTATION', + TF_SEGMENTATION = 'TF_SEGMENTATION', ANALYTICS = 'ANALYTICS', } @@ -81,12 +83,6 @@ export interface PluginsState { }; } -export interface TaskState { - task: Task | null; - taskFetchingError: any; - taskUpdatingError: any; -} - export interface UsersState { users: any[]; initialized: boolean; @@ -108,3 +104,54 @@ export interface ShareState { root: ShareItem; error: any; } + +export interface Model { + id: number | null; // null for preinstalled models + ownerID: number | null; // null for preinstalled models + name: string; + primary: boolean; + uploadDate: string; + updateDate: string; + labels: string[]; +} + +export interface Running { + [tid: string]: { + status: string; + processId: string; + error: any; + }; +} + +export interface ModelsState { + initialized: boolean; + creatingStatus: string; + creatingError: any; + startingError: any; + fetchingError: any; + deletingErrors: { // by id + [index: number]: any; + }; + models: Model[]; + runnings: Running[]; + visibleRunWindows: boolean; + activeRunTask: any; +} + +export interface ModelFiles { + [key: string]: string | File; + xml: string | File; + bin: string | File; + py: string | File; + json: string | File; +} + +export interface CombinedState { + auth: AuthState; + tasks: TasksState; + users: UsersState; + share: ShareState; + formats: FormatsState; + plugins: PluginsState; + models: ModelsState; +} diff --git a/cvat-ui/src/reducers/models-reducer.ts b/cvat-ui/src/reducers/models-reducer.ts new file mode 100644 index 00000000000..ec241ac530b --- /dev/null +++ b/cvat-ui/src/reducers/models-reducer.ts @@ -0,0 +1,125 @@ +import { AnyAction } from 'redux'; + +import { ModelsActionTypes } from '../actions/models-actions'; +import { ModelsState } from './interfaces'; + +const defaultState: ModelsState = { + initialized: false, + creatingStatus: '', + creatingError: null, + startingError: null, + fetchingError: null, + deletingErrors: {}, + models: [], + visibleRunWindows: false, + activeRunTask: null, + runnings: [], +}; + +export default function (state = defaultState, action: AnyAction): ModelsState { + switch (action.type) { + case ModelsActionTypes.GET_MODELS: { + return { + ...state, + fetchingError: null, + initialized: false, + }; + } + case ModelsActionTypes.GET_MODELS_SUCCESS: { + return { + ...state, + models: action.payload.models, + initialized: true, + }; + } + case ModelsActionTypes.GET_MODELS_FAILED: { + return { + ...state, + fetchingError: action.payload.error, + initialized: true, + }; + } + case ModelsActionTypes.DELETE_MODEL: { + const errors = { ...state.deletingErrors }; + delete errors[action.payload.id]; + return { + ...state, + deletingErrors: errors, + }; + } + case ModelsActionTypes.DELETE_MODEL_SUCCESS: { + return { + ...state, + models: state.models.filter( + (model): boolean => model.id !== action.payload.id, + ), + }; + } + 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: '', + }; + } + case ModelsActionTypes.CREATE_MODEL_STATUS_UPDATED: { + return { + ...state, + creatingStatus: action.payload.status, + }; + } + case ModelsActionTypes.CREATE_MODEL_FAILED: { + return { + ...state, + creatingError: action.payload.error, + creatingStatus: '', + }; + } + case ModelsActionTypes.CREATE_MODEL_SUCCESS: { + return { + ...state, + initialized: false, + creatingStatus: 'CREATED', + }; + } + case ModelsActionTypes.INFER_MODEL: { + return { + ...state, + startingError: null, + }; + } + case ModelsActionTypes.INFER_MODEL_FAILED: { + return { + ...state, + startingError: action.payload.error, + }; + } + case ModelsActionTypes.SHOW_RUN_MODEL_DIALOG: { + return { + ...state, + visibleRunWindows: true, + activeRunTask: action.payload.taskInstance, + }; + } + case ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG: { + return { + ...state, + visibleRunWindows: false, + activeRunTask: null, + }; + } + default: { + return { + ...state, + }; + } + } +} diff --git a/cvat-ui/src/reducers/plugins-reducer.ts b/cvat-ui/src/reducers/plugins-reducer.ts index 0cb717e8c2a..f594023c813 100644 --- a/cvat-ui/src/reducers/plugins-reducer.ts +++ b/cvat-ui/src/reducers/plugins-reducer.ts @@ -1,25 +1,31 @@ import { AnyAction } from 'redux'; import { PluginsActionTypes } from '../actions/plugins-actions'; - +import { registerGitPlugin } from '../utils/git-utils'; import { PluginsState, } from './interfaces'; + const defaultState: PluginsState = { initialized: false, plugins: { GIT_INTEGRATION: false, AUTO_ANNOTATION: false, TF_ANNOTATION: false, + TF_SEGMENTATION: false, ANALYTICS: false, }, }; - export default function (state = defaultState, action: AnyAction): PluginsState { switch (action.type) { case PluginsActionTypes.CHECKED_ALL_PLUGINS: { const { plugins } = action.payload; + + if (!state.plugins.GIT_INTEGRATION && plugins.GIT_INTEGRATION) { + registerGitPlugin(); + } + return { ...state, initialized: true, diff --git a/cvat-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index a4ae7408207..d6073be0f75 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -5,27 +5,7 @@ import usersReducer from './users-reducer'; import shareReducer from './share-reducer'; import formatsReducer from './formats-reducer'; import pluginsReducer from './plugins-reducer'; -import taskReducer from './task-reducer'; - -import { - AuthState, - TasksState, - UsersState, - ShareState, - FormatsState, - PluginsState, - TaskState, -} from './interfaces'; - -export interface CombinedState { - auth: AuthState; - tasks: TasksState; - users: UsersState; - share: ShareState; - formats: FormatsState; - plugins: PluginsState; - activeTask: TaskState; -} +import modelsReducer from './models-reducer'; export default function createRootReducer(): Reducer { return combineReducers({ @@ -35,6 +15,6 @@ export default function createRootReducer(): Reducer { share: shareReducer, formats: formatsReducer, plugins: pluginsReducer, - activeTask: taskReducer, + models: modelsReducer, }); } diff --git a/cvat-ui/src/reducers/task-reducer.ts b/cvat-ui/src/reducers/task-reducer.ts deleted file mode 100644 index c91aa821e44..00000000000 --- a/cvat-ui/src/reducers/task-reducer.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { AnyAction } from 'redux'; - -import { TaskActionTypes } from '../actions/task-actions'; -import { Task, TaskState } from './interfaces'; - -const defaultState: TaskState = { - taskFetchingError: null, - taskUpdatingError: null, - task: null, -}; - -export default function (state = defaultState, action: AnyAction): TaskState { - switch (action.type) { - case TaskActionTypes.GET_TASK: - return { - ...state, - taskFetchingError: null, - taskUpdatingError: null, - }; - case TaskActionTypes.GET_TASK_SUCCESS: { - return { - ...state, - task: { - instance: action.payload.taskInstance, - preview: action.payload.previewImage, - }, - }; - } - case TaskActionTypes.GET_TASK_FAILED: { - return { - ...state, - task: null, - taskFetchingError: action.payload.error, - }; - } - case TaskActionTypes.UPDATE_TASK: { - return { - ...state, - taskUpdatingError: null, - taskFetchingError: null, - }; - } - case TaskActionTypes.UPDATE_TASK_SUCCESS: { - return { - ...state, - task: { - ...(state.task as Task), - instance: action.payload.taskInstance, - }, - }; - } - case TaskActionTypes.UPDATE_TASK_FAILED: { - return { - ...state, - task: { - ...(state.task as Task), - instance: action.payload.taskInstance, - }, - taskUpdatingError: action.payload.error, - }; - } - default: - return { ...state }; - } -} diff --git a/cvat-ui/src/reducers/tasks-reducer.ts b/cvat-ui/src/reducers/tasks-reducer.ts index 94e18baad46..ea27bfa3246 100644 --- a/cvat-ui/src/reducers/tasks-reducer.ts +++ b/cvat-ui/src/reducers/tasks-reducer.ts @@ -6,6 +6,7 @@ import { TasksState, Task } from './interfaces'; const defaultState: TasksState = { initialized: false, tasksFetchingError: null, + taskUpdatingError: null, count: 0, current: [], gettingQuery: { @@ -44,6 +45,7 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks return { ...stateToResetErrors, tasksFetchingError: null, + taskUpdatingError: null, activities: { ...stateToResetErrors.activities, dumps: { @@ -55,6 +57,10 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks loadingError: null, loadingDoneMessage: '', }, + deletes: { + ...stateToResetErrors.activities.deletes, + deletingError: null, + }, }, }; } @@ -339,6 +345,43 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks }, }; } + case TasksActionTypes.UPDATE_TASK: { + return { + ...state, + taskUpdatingError: null, + }; + } + case TasksActionTypes.UPDATE_TASK_SUCCESS: { + return { + ...state, + current: state.current.map((task): Task => { + if (task.instance.id === action.payload.taskInstance.id) { + return { + ...task, + instance: action.payload.taskInstance, + }; + } + + return task; + }), + }; + } + 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 { + ...task, + instance: action.payload.taskInstance, + }; + } + + return task; + }), + }; + } default: return state; } diff --git a/cvat-ui/src/store.ts b/cvat-ui/src/store.ts index 89fd5fc6659..e87aa054ec9 100644 --- a/cvat-ui/src/store.ts +++ b/cvat-ui/src/store.ts @@ -1,15 +1,28 @@ import thunk from 'redux-thunk'; -import { createStore, applyMiddleware, Store } from 'redux'; - -import createRootReducer from './reducers/root-reducer'; +import { + createStore, + applyMiddleware, + Store, + Reducer, +} from 'redux'; const middlewares = [ thunk, ]; -export default function createCVATStore(): Store { - return createStore( +let store: Store | null = null; + +export default function createCVATStore(createRootReducer: () => Reducer): void { + store = createStore( createRootReducer(), applyMiddleware(...middlewares), ); } + +export function getCVATStore(): Store { + if (store) { + return store; + } + + throw new Error('First create a store'); +} diff --git a/cvat-ui/src/stylesheet.css b/cvat-ui/src/stylesheet.css index ee46cbdf776..c170431dc9d 100644 --- a/cvat-ui/src/stylesheet.css +++ b/cvat-ui/src/stylesheet.css @@ -1,11 +1,6 @@ -#root { - width: 100%; - height: 100%; - min-height: 100%; - display: grid; -} - .cvat-header.ant-layout-header { + display: -webkit-box; + display: -ms-flexbox; display: flex; padding-left: 0px; padding-right: 0px; @@ -16,24 +11,40 @@ .cvat-left-header { width: 50%; + display: -webkit-box; + display: -ms-flexbox; display: flex; - justify-content: flex-start; - align-items: center; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; } .cvat-right-header { width: 50%; + display: -webkit-box; + display: -ms-flexbox; display: flex; - justify-content: flex-end; - align-items: center; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; } .cvat-flex { + display: -webkit-box; + display: -ms-flexbox; display: flex; } .cvat-flex-center { - align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; } .cvat-black-color { @@ -52,18 +63,26 @@ } .anticon.cvat-logo-icon { + display: -webkit-box; + display: -ms-flexbox; display: flex; - align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; margin-right: 20px; } .anticon.cvat-logo-icon > img { + display: -webkit-box; + display: -ms-flexbox; display: flex; height: 15.8px; margin-left: 18px; } .anticon.cvat-back-icon > img { + display: -webkit-box; + display: -ms-flexbox; display: flex; height: 15.8px; margin-left: 18px; @@ -71,19 +90,24 @@ } .anticon.cvat-back-icon:hover > img { - filter: invert(0.4); + -webkit-filter: invert(0.4); + filter: invert(0.4); } .ant-btn.cvat-header-button { - color: black !important; + color: black; padding: 0px 10px; margin-right: 10px; } .ant-menu.cvat-header-menu { + width: -webkit-fit-content; + width: -moz-fit-content; width: fit-content; - align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; border: 0px; border-left: 1px solid #c3c3c3; height: 100%; @@ -91,7 +115,7 @@ } .ant-menu.cvat-header-menu > li.ant-menu-submenu { - border-bottom: unset !important; + border-bottom: unset; } @@ -115,7 +139,9 @@ .anticon.cvat-header-menu-icon > img { width: 14px; - transform: rotate(-90deg); + -webkit-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + transform: rotate(-90deg); } .anticon.cvat-empty-tasks-icon > img { @@ -142,12 +168,18 @@ } .cvat-tasks-page > div:nth-child(2) > div:nth-child(1) { + display: -webkit-box; + display: -ms-flexbox; display: flex; } .cvat-tasks-page > div:nth-child(2) > div:nth-child(2) { + display: -webkit-box; + display: -ms-flexbox; display: flex; - justify-content: flex-end; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; } .cvat-tasks-page > div:nth-child(2) > div:nth-child(1) > span:nth-child(2) { @@ -156,7 +188,10 @@ } .cvat-tasks-page > span.ant-typography { - user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .cvat-tasks-page { @@ -213,8 +248,12 @@ } .ant-pagination.cvat-tasks-pagination { + display: -webkit-box; + display: -ms-flexbox; display: flex; - justify-content: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; } .cvat-task-list { @@ -246,6 +285,8 @@ } .cvat-tasks-list-item > div:nth-child(4) > div:nth-child(2) > div { + display: -webkit-box; + display: -ms-flexbox; display: flex; } @@ -254,23 +295,33 @@ } .cvat-task-item-menu-icon > img:hover { - filter: invert(0.5); + -webkit-filter: invert(0.5); + filter: invert(0.5); } -.cvat-task-item-menu > hr { +.ant-menu.cvat-actions-menu { + -webkit-box-shadow: 0 0 17px rgba(0,0,0,0.2); + box-shadow: 0 0 17px rgba(0,0,0,0.2); +} + +.cvat-actions-menu > hr { border: 0.5px solid #D6D6D6; } -.cvat-task-item-load-submenu-item { - padding: 0px; +.cvat-actions-menu > *:hover { + background-color: rgba(24,144,255,0.05); } -.cvat-task-item-load-submenu-item > span > .ant-upload { - width: 100%; +.cvat-actions-menu-load-submenu-item:hover { + background-color: rgba(24,144,255,0.05); } -.cvat-task-item-dump-submenu-item { - padding: 0px; +.cvat-actions-menu-dump-submenu-item:hover { + background-color: rgba(24,144,255,0.05); +} + +.cvat-actions-menu > li:nth-child(2) > div > span { + margin-right: 15px; } .cvat-task-item-preview { @@ -279,8 +330,12 @@ } .cvat-task-item-preview-wrapper { + display: -webkit-box; + display: -ms-flexbox; display: flex; - justify-content: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; overflow: hidden; margin: 20px; margin-top: 0px; @@ -296,8 +351,8 @@ margin-right: 5px; } -.cvat-task-completed-progress > div > div > div > div { - background: #52c41a !important; +.cvat-task-completed-progress > div > div > div > div.ant-progress-bg { + background: #52c41a !important; /* csslint allow: important */ } .cvat-task-progress-progress { @@ -334,6 +389,26 @@ margin-top: 20px; } +.cvat-dataset-repository-url > a { + margin-right: 10px; +} + +.cvat-dataset-repository-url > .ant-tag-red { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + opacity: 0.4; +} + +.cvat-dataset-repository-url > .ant-tag-red:hover { + opacity: 0.8; +} + +.cvat-dataset-repository-url > .ant-tag-red:active { + opacity: 1; +} + .cvat-task-job-list { width: 100%; height: auto; @@ -355,8 +430,12 @@ } .cvat-task-preview-wrapper { + display: -webkit-box; + display: -ms-flexbox; display: flex; - justify-content: start; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: start; overflow: hidden; margin-bottom: 20px; } @@ -370,36 +449,62 @@ margin-left: 15px; } -.cvat-raw-labels-viewer { - border-color: #d9d9d9 !important; - box-shadow: none !important; +textarea.ant-input.cvat-raw-labels-viewer { + border-color: #d9d9d9; + -webkit-box-shadow: none; + box-shadow: none; border-top: none; border-radius: 0px 0px 5px 5px; - min-height: 9em !important; + min-height: 9em; font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; } +.cvat-raw-labels-viewer:focus { + border-color: #d9d9d9; + -webkit-box-shadow: none; + box-shadow: none; +} + +.cvat-raw-labels-viewer:hover { + border-color: #d9d9d9; + -webkit-box-shadow: none; + box-shadow: none; +} + .cvat-constructor-viewer { border: 1px solid #d9d9d9; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; border-top: none; border-radius: 0px 0px 5px 5px; padding: 5px; + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; overflow-y: auto; min-height: 9em; } .cvat-constructor-viewer-item { + height: -webkit-fit-content; + height: -moz-fit-content; height: fit-content; + display: -webkit-box; + display: -ms-flexbox; display: flex; - align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; padding: 2px 10px; border-radius: 2px; margin: 2px; margin-left: 8px; - user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; border: 1px solid rgba(0, 0, 0, 0); opacity: 0.6; } @@ -419,14 +524,23 @@ } .cvat-constructor-viewer-new-item { + height: -webkit-fit-content; + height: -moz-fit-content; height: fit-content; + display: -webkit-box; + display: -ms-flexbox; display: flex; - align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; padding: 2px 10px; border-radius: 2px; margin: 2px; margin-left: 8px; - user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; opacity: 1; } @@ -466,8 +580,8 @@ .cvat-task-jobs-table > div > div { text-align: center; } -.cvat-task-jobs-table > div > div > ul { - float: none !important; +.cvat-task-jobs-table > div > div > .ant-table-pagination.ant-pagination { + float: none; } .cvat-job-completed-color { @@ -514,12 +628,167 @@ } .cvat-share-tree { + height: -webkit-fit-content; + height: -moz-fit-content; height: fit-content; min-height: 10em; max-height: 20em; overflow: auto; } +.cvat-models-page { + padding-top: 30px; + height: 100%; + overflow: auto; +} + +.cvat-models-page > div:nth-child(1) { + margin-bottom: 10px; +} + +.cvat-models-page > div:nth-child(1) > div:nth-child(1) { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +.cvat-models-page > div:nth-child(1) > div:nth-child(2) { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +/* empty-models icon */ +.cvat-empty-models-list > div:nth-child(1) { + margin-top: 50px; +} + +.cvat-empty-models-list > div:nth-child(2) > div { + margin-top: 20px; +} + +/* No models uploaded yet */ +.cvat-empty-models-list > div:nth-child(2) > div > span { + font-size: 20px; + color: #4A4A4A; +} + +/* To annotate your task automatically */ +.cvat-empty-models-list > div:nth-child(3) { + margin-top: 10px; +} + +.cvat-models-list { + height: 100%; + overflow-y: auto; +} + +.cvat-models-list-item { + width: 100%; + height: 60px; + border: 1px solid #c3c3c3; + border-radius: 3px; + margin-bottom: 15px; + padding: 15px; + background: white; +} + +.cvat-models-list-item:hover { + border: 1px solid #40a9ff; +} + +.cvat-models-list-item > div { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.cvat-models-list-item > div:nth-child(2) > span { + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.cvat-models-list-item > div:nth-child(3) > span { + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.anticon.cvat-empty-models-icon > img { + width: 90px; +} + +.cvat-create-model-form-wrapper { + text-align: center; + margin-top: 40px; + overflow-y: auto; + height: 90%; +} + +.cvat-create-model-form-wrapper > div > span { + font-size: 36px; +} + +.cvat-create-model-content { + margin-top: 20px; + width: 100%; + height: auto; + border: 1px solid #c3c3c3; + border-radius: 3px; + padding: 20px; + background: white; + text-align: initial; +} + +.cvat-create-model-content > div:nth-child(1) > i { + float: right; + font-size: 20px; + color: red; +} + +.cvat-create-model-content > div:nth-child(4) { + margin-top: 10px; +} + +.cvat-create-model-content > div:nth-child(5) > button { + margin-top: 10px; + float: right; + width: 120px; +} + +.cvat-run-model-dialog > div:not(first-child) { + margin-top: 10px; +} + +.cvat-run-model-dialog-info-icon { + color: blue; +} + +.cvat-run-model-dialog-remove-mapping-icon { + color: red; +} + +#root { + width: 100%; + height: 100%; + min-height: 100%; + display: -ms-grid; + display: grid; +} + #cvat-create-task-button { padding: 0 30px; } + +#cvat-create-model-button { + padding: 0 30px; +} diff --git a/cvat-ui/src/utils/git-utils.ts b/cvat-ui/src/utils/git-utils.ts new file mode 100644 index 00000000000..6b84a57abdf --- /dev/null +++ b/cvat-ui/src/utils/git-utils.ts @@ -0,0 +1,191 @@ +import getCore from '../core'; + +const core = getCore(); +const baseURL = core.config.backendAPI.slice(0, -7); + +interface GitPlugin { + name: string; + description: string; + cvat: { + classes: { + Task: { + prototype: { + save: { + leave: (plugin: GitPlugin, task: any) => void; + }; + }; + }; + }; + }; + data: { + task: any; + lfs: boolean; + repos: string; + }; + callbacks: { + onStatusChange: ((status: string) => void) | null; + }; +} + +interface ReposData { + url: string; + status: { + value: 'sync' | '!sync' | 'merged'; + error: string | null; + }; +} + +function waitForClone(cloneResponse: any): Promise { + return new Promise((resolve, reject): void => { + async function checkCallback(): Promise { + core.server.request( + `${baseURL}/git/repository/check/${cloneResponse.rq_id}`, + { + method: 'GET', + }, + ).then((response: any): void => { + if (['queued', 'started'].includes(response.status)) { + setTimeout(checkCallback, 1000); + } else if (response.status === 'finished') { + resolve(); + } else if (response.status === 'failed') { + let message = 'Repository status check failed. '; + if (response.stderr) { + message += response.stderr; + } + + reject(message); + } else { + const message = `Repository status check returned the status "${response.status}"`; + reject(message); + } + }).catch((error: any): void => { + const message = `Can not sent a request to clone the repository. ${error.toString()}`; + reject(message); + }); + } + + setTimeout(checkCallback, 1000); + }); +} + +async function cloneRepository( + this: any, + plugin: GitPlugin, + createdTask: any, +): Promise { + return new Promise((resolve, reject): void => { + if (typeof (this.id) !== 'undefined' || plugin.data.task !== this) { + // not the first save, we do not need to clone the repository + // or anchor set for another task + resolve(); + } else if (plugin.data.repos) { + if (plugin.callbacks.onStatusChange) { + plugin.callbacks.onStatusChange('The repository is being cloned..'); + } + + core.server.request(`${baseURL}/git/repository/create/${createdTask.id}`, { + method: 'POST', + headers: { + 'Content-type': 'application/json', + }, + data: JSON.stringify({ + path: plugin.data.repos, + lfs: plugin.data.lfs, + tid: createdTask.id, + }), + }).then(waitForClone).then((): void => { + resolve(); + }).catch((error: any): void => { + createdTask.delete().finally((): void => { + reject( + new core.exceptions.PluginError( + typeof (error) === 'string' + ? error : error.message, + ), + ); + }); + }); + } + }); +} + +export function registerGitPlugin(): void { + const plugin: GitPlugin = { + name: 'Git', + description: 'Plugin allows to work with git repositories', + cvat: { + classes: { + Task: { + prototype: { + save: { + leave: cloneRepository, + }, + }, + }, + }, + }, + data: { + task: null, + lfs: false, + repos: '', + }, + callbacks: { + onStatusChange: null, + }, + }; + + core.plugins.register(plugin); +} + +export async function getReposData(tid: number): Promise { + const response = await core.server.request( + `${baseURL}/git/repository/get/${tid}`, + { + method: 'GET', + }, + ); + + if (!response.url.value) { + return null; + } + + return { + url: response.url.value.split(/\s+/)[0], + status: { + value: response.status.value, + error: response.status.error, + }, + }; +} + +export function syncRepos(tid: number): Promise { + return new Promise((resolve, reject): void => { + core.server.request(`${baseURL}/git/repository/push/${tid}`, { + method: 'GET', + }).then((syncResponse: any): void => { + async function checkSync(): Promise { + const id = syncResponse.rq_id; + const response = await core.server.request(`${baseURL}/git/repository/check/${id}`, { + method: 'GET', + }); + + if (['queued', 'started'].includes(response.status)) { + setTimeout(checkSync, 1000); + } else if (response.status === 'finished') { + resolve(); + } else if (response.status === 'failed') { + const message = `Can not push to remote repository. Message: ${response.stderr}`; + throw new Error(message); + } else { + const message = `Check returned status "${response.status}".`; + throw new Error(message); + } + } + + setTimeout(checkSync, 1000); + }).catch((error: any): void => { + reject(error); + }); + }); +} diff --git a/cvat-ui/src/utils/plugin-checker.ts b/cvat-ui/src/utils/plugin-checker.ts index edda0593f3b..fb28e9fbebd 100644 --- a/cvat-ui/src/utils/plugin-checker.ts +++ b/cvat-ui/src/utils/plugin-checker.ts @@ -30,6 +30,13 @@ class PluginChecker { } return false; } + case SupportedPlugins.TF_SEGMENTATION: { + const response = await fetch(`${serverHost}/tensorflow/segmentation/meta/get`); + if (response.ok) { + return true; + } + return false; + } case SupportedPlugins.ANALYTICS: { const response = await fetch(`${serverHost}/analytics/app/kibana`); if (response.ok) { diff --git a/cvat-ui/src/utils/validation-patterns.ts b/cvat-ui/src/utils/validation-patterns.ts index f3761ac596c..ea77cf3547f 100644 --- a/cvat-ui/src/utils/validation-patterns.ts +++ b/cvat-ui/src/utils/validation-patterns.ts @@ -52,9 +52,16 @@ const validationPatterns = { }, validateURL: { - pattern: /^(https?):\/\/[^\s$.?#].[^\s]*$/, + // eslint-disable-next-line + pattern: /^((https?:\/\/)|(git@))[^\s$.?#].[^\s]*$/, // url, ssh url, ip message: 'URL is not valid', }, + + validatePath: { + // eslint-disable-next-line + pattern: /^\[\/?([A-z0-9-_+]+\/)*([A-z0-9]+\.(xml|zip|json))\]$/, + message: 'Git path is not valid', + }, }; export default { ...validationPatterns }; diff --git a/cvat/apps/auto_annotation/views.py b/cvat/apps/auto_annotation/views.py index e13ed2c0766..ba858a75182 100644 --- a/cvat/apps/auto_annotation/views.py +++ b/cvat/apps/auto_annotation/views.py @@ -147,6 +147,7 @@ def get_meta_info(request): "uploadDate": dl_model.created_date, "updateDate": dl_model.updated_date, "labels": labels, + "owner": dl_model.owner.id, }) queue = django_rq.get_queue("low") diff --git a/cvat/apps/git/git.py b/cvat/apps/git/git.py index 5edb721421f..1874b25ce5f 100644 --- a/cvat/apps/git/git.py +++ b/cvat/apps/git/git.py @@ -433,11 +433,12 @@ def get(tid, user): response['status']['value'] = str(db_git.status) except git.exc.GitCommandError as ex: _have_no_access_exception(ex) + db_git.save() except Exception as ex: db_git.status = GitStatusChoice.NON_SYNCED + db_git.save() response['status']['error'] = str(ex) - db_git.save() return response def update_states():