From 3775bc25578888a2b12a05218df253398a926909 Mon Sep 17 00:00:00 2001 From: Kirill Lakhov Date: Mon, 30 Jan 2023 22:44:51 +0300 Subject: [PATCH] New models UI (#5635) --- CHANGELOG.md | 1 + cvat-core/package.json | 2 +- cvat-core/src/api-implementation.ts | 3 +- cvat-core/src/api.ts | 12 +- cvat-core/src/enums.ts | 17 +- cvat-core/src/lambda-manager.ts | 69 +++++-- cvat-core/src/ml-model.ts | 139 +++++++++++-- cvat-core/src/server-proxy.ts | 178 ++++++++++++++++- cvat-ui/package.json | 2 +- cvat-ui/src/actions/annotation-actions.ts | 5 +- cvat-ui/src/actions/cloud-storage-actions.ts | 11 +- cvat-ui/src/actions/jobs-actions.ts | 13 +- cvat-ui/src/actions/models-actions.ts | 120 ++++++++++-- cvat-ui/src/actions/projects-actions.ts | 16 +- cvat-ui/src/actions/tasks-actions.ts | 13 +- cvat-ui/src/actions/webhooks-actions.ts | 13 +- .../controls-side-bar/tools-control.tsx | 41 ++-- cvat-ui/src/components/common/preview.tsx | 12 +- .../create-model-page/create-model-page.tsx | 48 +++++ .../create-model-page/model-form.tsx | 162 +++++++++++++++ .../components/create-model-page/styles.scss | 46 +++++ cvat-ui/src/components/cvat-app.tsx | 16 +- .../model-runner-modal/detector-runner.tsx | 66 ++++--- .../model-runner-dialog.tsx | 20 +- .../models-page/deployed-model-item.tsx | 174 ++++++++++++---- .../models-page/deployed-models-list.tsx | 47 ++--- .../src/components/models-page/empty-list.tsx | 1 + .../models-page/model-provider-icon.tsx | 29 +++ .../models-page/models-action-menu.tsx | 66 +++++++ .../models-filter-configuration.tsx | 55 ++++++ .../components/models-page/models-page.tsx | 86 ++++++-- .../src/components/models-page/styles.scss | 185 +++++++++++++++++- .../src/components/models-page/top-bar.tsx | 89 +++++++++ .../resource-sorting-filtering/filtering.tsx | 7 +- .../resource-sorting-filtering/sorting.tsx | 5 +- .../containers/models-page/models-page.tsx | 31 --- cvat-ui/src/cvat-core-wrapper.ts | 12 +- cvat-ui/src/reducers/index.ts | 50 ++--- cvat-ui/src/reducers/models-reducer.ts | 139 ++++++++++++- cvat-ui/src/reducers/notifications-reducer.ts | 33 ++++ cvat-ui/src/utils/filter-null.ts | 17 ++ 41 files changed, 1748 insertions(+), 303 deletions(-) create mode 100644 cvat-ui/src/components/create-model-page/create-model-page.tsx create mode 100644 cvat-ui/src/components/create-model-page/model-form.tsx create mode 100644 cvat-ui/src/components/create-model-page/styles.scss create mode 100644 cvat-ui/src/components/models-page/model-provider-icon.tsx create mode 100644 cvat-ui/src/components/models-page/models-action-menu.tsx create mode 100644 cvat-ui/src/components/models-page/models-filter-configuration.tsx create mode 100644 cvat-ui/src/components/models-page/top-bar.tsx delete mode 100644 cvat-ui/src/containers/models-page/models-page.tsx create mode 100644 cvat-ui/src/utils/filter-null.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index da4fb87a..aa93150a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Windows Installation Instructions adjusted to work around - The contour detection function for semantic segmentation () - Delete newline character when generating a webhook signature () +- DL models UI () ### Deprecated - TDB diff --git a/cvat-core/package.json b/cvat-core/package.json index 99ad94c1..a85702d8 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "8.0.0", + "version": "8.1.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 5a018f14..621ce8f8 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -37,6 +37,7 @@ export default function implementAPI(cvat) { cvat.lambda.cancel.implementation = lambdaManager.cancel.bind(lambdaManager); cvat.lambda.listen.implementation = lambdaManager.listen.bind(lambdaManager); cvat.lambda.requests.implementation = lambdaManager.requests.bind(lambdaManager); + cvat.lambda.providers.implementation = lambdaManager.providers.bind(lambdaManager); cvat.server.about.implementation = async () => { const result = await serverProxy.server.about(); diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index bff5ed74..ec7488d0 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -190,18 +190,22 @@ function build() { const result = await PluginRegistry.apiWrapper(cvat.lambda.call, task, model, args); return result; }, - async cancel(requestID) { - const result = await PluginRegistry.apiWrapper(cvat.lambda.cancel, requestID); + async cancel(requestID, functionID) { + const result = await PluginRegistry.apiWrapper(cvat.lambda.cancel, requestID, functionID); return result; }, - async listen(requestID, onChange) { - const result = await PluginRegistry.apiWrapper(cvat.lambda.listen, requestID, onChange); + async listen(requestID, functionID, onChange) { + const result = await PluginRegistry.apiWrapper(cvat.lambda.listen, requestID, functionID, onChange); return result; }, async requests() { const result = await PluginRegistry.apiWrapper(cvat.lambda.requests); return result; }, + async providers() { + const result = await PluginRegistry.apiWrapper(cvat.lambda.providers); + return result; + }, }, logger: loggerStorage, config: { diff --git a/cvat-core/src/enums.ts b/cvat-core/src/enums.ts index 1990dc1a..386f4145 100644 --- a/cvat-core/src/enums.ts +++ b/cvat-core/src/enums.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier = MIT @@ -132,10 +132,23 @@ export enum HistoryActions { RESTORED_FRAME = 'Restored frame', } -export enum ModelType { +export enum ModelKind { DETECTOR = 'detector', INTERACTOR = 'interactor', TRACKER = 'tracker', + CLASSIFIER = 'classifier', + REID = 'reid', +} + +export enum ModelProviders { + CVAT = 'cvat', +} + +export enum ModelReturnType { + RECTANGLE = 'rectangle', + TAG = 'tag', + POLYGON = 'polygon', + MASK = 'mask', } export const colors = [ diff --git a/cvat-core/src/lambda-manager.ts b/cvat-core/src/lambda-manager.ts index 184723ab..c186190b 100644 --- a/cvat-core/src/lambda-manager.ts +++ b/cvat-core/src/lambda-manager.ts @@ -1,12 +1,25 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import serverProxy from './server-proxy'; import { ArgumentError } from './exceptions'; import MLModel from './ml-model'; -import { RQStatus } from './enums'; +import { ModelProviders, RQStatus } from './enums'; + +export interface ModelProvider { + name: string; + icon: string; + attributes: Record; +} + +interface ModelProxy { + run: (body: any) => Promise; + call: (modelID: string | number, body: any) => Promise; + status: (requestID: string) => Promise; + cancel: (requestID: string) => Promise; +} class LambdaManager { private listening: any; @@ -18,18 +31,16 @@ class LambdaManager { } async list(): Promise { - if (Array.isArray(this.cachedList)) { - return [...this.cachedList]; - } + const lambdaFunctions = await serverProxy.lambda.list(); + const functions = await serverProxy.functions.list(); - const result = await serverProxy.lambda.list(); + const result = [...lambdaFunctions, ...functions]; const models = []; for (const model of result) { models.push( new MLModel({ ...model, - type: model.kind, }), ); } @@ -59,7 +70,7 @@ class LambdaManager { function: model.id, }; - const result = await serverProxy.lambda.run(body); + const result = await LambdaManager.getModelProxy(model).run(body); return result.id; } @@ -73,32 +84,43 @@ class LambdaManager { task: taskID, }; - const result = await serverProxy.lambda.call(model.id, body); + const result = await LambdaManager.getModelProxy(model).call(model.id, body); return result; } async requests() { - const result = await serverProxy.lambda.requests(); + const lambdaRequests = await serverProxy.lambda.requests(); + const functionsRequests = await serverProxy.functions.requests(); + const result = [...lambdaRequests, ...functionsRequests]; return result.filter((request) => ['queued', 'started'].includes(request.status)); } - async cancel(requestID): Promise { + async cancel(requestID, functionID): Promise { if (typeof requestID !== 'string') { throw new ArgumentError(`Request id argument is required to be a string. But got ${requestID}`); } + const model = this.cachedList.find((_model) => _model.id === functionID); + if (!model) { + throw new ArgumentError('Incorrect Function Id provided'); + } if (this.listening[requestID]) { clearTimeout(this.listening[requestID].timeout); delete this.listening[requestID]; } - await serverProxy.lambda.cancel(requestID); + + await LambdaManager.getModelProxy(model).cancel(requestID); } - async listen(requestID, onUpdate): Promise { + async listen(requestID, functionID, onUpdate): Promise { + const model = this.cachedList.find((_model) => _model.id === functionID); + if (!model) { + throw new ArgumentError('Incorrect Function Id provided'); + } const timeoutCallback = async (): Promise => { try { this.listening[requestID].timeout = null; - const response = await serverProxy.lambda.status(requestID); + const response = await LambdaManager.getModelProxy(model).status(requestID); if (response.status === RQStatus.QUEUED || response.status === RQStatus.STARTED) { onUpdate(response.status, response.progress || 0); @@ -123,9 +145,28 @@ class LambdaManager { this.listening[requestID] = { onUpdate, + functionID, timeout: setTimeout(timeoutCallback, 2000), }; } + + async providers(): Promise { + const providersData: Record> = await serverProxy.functions.providers(); + const providers = Object.entries(providersData).map(([provider, attributes]) => { + const { icon } = attributes; + delete attributes.icon; + return { + name: provider, + icon, + attributes, + }; + }); + return providers; + } + + private static getModelProxy(model: MLModel): ModelProxy { + return model.provider === ModelProviders.CVAT ? serverProxy.lambda : serverProxy.functions; + } } export default new LambdaManager(); diff --git a/cvat-core/src/ml-model.ts b/cvat-core/src/ml-model.ts index 13cdd102..fb605235 100644 --- a/cvat-core/src/ml-model.ts +++ b/cvat-core/src/ml-model.ts @@ -1,9 +1,12 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import { ModelType } from './enums'; +import { isBrowser, isNode } from 'browser-or-node'; +import serverProxy from './server-proxy'; +import PluginRegistry from './plugins'; +import { ModelProviders, ModelKind, ModelReturnType } from './enums'; interface ModelAttribute { name: string; @@ -26,19 +29,27 @@ interface ModelTip { } interface SerializedModel { - id: string; - name: string; - labels: string[]; - version: number; - attributes: Record; - framework: string; - description: string; - type: ModelType; + id?: string | number; + name?: string; + labels?: string[]; + version?: number; + attributes?: Record; + framework?: string; + description?: string; + kind?: ModelKind; + type?: string; + return_type?: ModelReturnType; + owner?: any; + provider?: string; + api_key?: string; + url?: string; help_message?: string; animated_gif?: string; min_pos_points?: number; min_neg_points?: number; startswith_box?: boolean; + created_date?: string; + updated_date?: string; } export default class MLModel { @@ -49,7 +60,7 @@ export default class MLModel { this.serialized = { ...serialized }; } - public get id(): string { + public get id(): string | number { return this.serialized.id; } @@ -77,8 +88,8 @@ export default class MLModel { return this.serialized.description; } - public get type(): ModelType { - return this.serialized.type; + public get kind(): ModelKind { + return this.serialized.kind; } public get params(): ModelParams { @@ -104,8 +115,110 @@ export default class MLModel { }; } + public get owner(): string { + return this.serialized?.owner?.username || ''; + } + + public get provider(): string { + return this.serialized?.provider || ModelProviders.CVAT; + } + + public get isDeletable(): boolean { + return this.provider !== ModelProviders.CVAT; + } + + public get createdDate(): string | undefined { + return this.serialized?.created_date; + } + + public get updatedDate(): string | undefined { + return this.serialized?.updated_date; + } + + public get url(): string | undefined { + return this.serialized?.url; + } + + public get returnType(): ModelReturnType | undefined { + return this.serialized?.return_type; + } + // Used to set a callback when the tool is blocked in UI public set onChangeToolsBlockerState(onChangeToolsBlockerState: (event: string) => void) { this.changeToolsBlockerStateCallback = onChangeToolsBlockerState; } + + public async save(): Promise { + const result = await PluginRegistry.apiWrapper.call(this, MLModel.prototype.save); + return result; + } + + public async delete(): Promise { + const result = await PluginRegistry.apiWrapper.call(this, MLModel.prototype.delete); + return result; + } + + public async getPreview(): Promise { + const result = await PluginRegistry.apiWrapper.call(this, MLModel.prototype.getPreview); + return result; + } } + +Object.defineProperties(MLModel.prototype.save, { + implementation: { + writable: false, + enumerable: false, + value: async function implementation(): Promise { + const modelData = { + provider: this.provider, + url: this.serialized.url, + api_key: this.serialized.api_key, + }; + + const model = await serverProxy.functions.create(modelData); + return new MLModel(model); + }, + }, +}); + +Object.defineProperties(MLModel.prototype.delete, { + implementation: { + writable: false, + enumerable: false, + value: async function implementation(): Promise { + if (this.isDeletable) { + await serverProxy.functions.delete(this.id); + } + }, + }, +}); + +Object.defineProperties(MLModel.prototype.getPreview, { + implementation: { + writable: false, + enumerable: false, + value: async function implementation(): Promise { + if (this.provider === ModelProviders.CVAT) { + return ''; + } + return new Promise((resolve, reject) => { + serverProxy.functions + .getPreview(this.id) + .then((result) => { + if (isNode) { + resolve(global.Buffer.from(result, 'binary').toString('base64')); + } else if (isBrowser) { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(result); + } + }) + .catch((error) => { + reject(error); + }); + }); + }, + }, +}); diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 725e87fe..21932d9f 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -1550,10 +1550,74 @@ async function getAnnotations(session, id) { } catch (errorData) { throw generateError(errorData); } + return response.data; +} + +async function getFunctions() { + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/functions`, { + proxy: config.proxy, + }); + return response.data.results; + } catch (errorData) { + if (errorData.response.status === 404) { + return []; + } + throw generateError(errorData); + } +} + +async function getFunctionPreview(modelID) { + const { backendAPI } = config; + + let response = null; + try { + const url = `${backendAPI}/functions/${modelID}/preview`; + response = await Axios.get(url, { + proxy: config.proxy, + responseType: 'blob', + }); + } catch (errorData) { + const code = errorData.response ? errorData.response.status : errorData.code; + throw new ServerError(`Could not get preview for the model ${modelID} from the server`, code); + } return response.data; } +async function getFunctionProviders() { + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/functions/info`, { + proxy: config.proxy, + }); + return response.data; + } catch (errorData) { + if (errorData.response.status === 404) { + return []; + } + throw generateError(errorData); + } +} + +async function deleteFunction(functionId: number) { + const { backendAPI } = config; + + try { + await Axios.delete(`${backendAPI}/functions/${functionId}`, { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); + } +} + // Session is 'task' or 'job' async function updateAnnotations(session, id, data, action) { const { backendAPI } = config; @@ -1580,10 +1644,26 @@ async function updateAnnotations(session, id, data, action) { } catch (errorData) { throw generateError(errorData); } - return response.data; } +async function runFunctionRequest(body) { + const { backendAPI } = config; + + try { + const response = await Axios.post(`${backendAPI}/functions/requests/`, JSON.stringify(body), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + + return response.data; + } catch (errorData) { + throw generateError(errorData); + } +} + // Session is 'task' or 'job' async function uploadAnnotations( session, @@ -1604,7 +1684,6 @@ async function uploadAnnotations( }; const url = `${backendAPI}/${session}s/${id}/annotations`; - async function wait() { return new Promise((resolve, reject) => { async function requestStatus() { @@ -1666,7 +1745,6 @@ async function uploadAnnotations( throw generateError(errorData); } } - try { return await wait(); } catch (errorData) { @@ -1674,6 +1752,19 @@ async function uploadAnnotations( } } +async function getFunctionRequestStatus(requestID) { + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/functions/requests/${requestID}`, { + proxy: config.proxy, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); + } +} + // Session is 'task' or 'job' async function dumpAnnotations(id, name, format) { const { backendAPI } = config; @@ -1703,11 +1794,40 @@ async function dumpAnnotations(id, name, format) { reject(generateError(errorData)); }); } - setTimeout(request); }); } +async function cancelFunctionRequest(requestId) { + const { backendAPI } = config; + + try { + await Axios.delete(`${backendAPI}/functions/requests/${requestId}`, { + method: 'DELETE', + }); + } catch (errorData) { + throw generateError(errorData); + } +} + +async function createFunction(functionData: any) { + const params = enableOrganization(); + const { backendAPI } = config; + + try { + const response = await Axios.post(`${backendAPI}/functions`, JSON.stringify(functionData), { + proxy: config.proxy, + params, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); + } +} + async function saveLogs(logs) { const { backendAPI } = config; @@ -1723,6 +1843,40 @@ async function saveLogs(logs) { } } +async function callFunction(funId, body) { + const { backendAPI } = config; + + try { + const response = await Axios.post(`${backendAPI}/functions/${funId}/run`, JSON.stringify(body), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + + return response.data; + } catch (errorData) { + throw generateError(errorData); + } +} + +async function getFunctionsRequests() { + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/functions/requests/`, { + proxy: config.proxy, + }); + + return response.data; + } catch (errorData) { + if (errorData.response.status === 404) { + return []; + } + throw generateError(errorData); + } +} + async function getLambdaFunctions() { const { backendAPI } = config; @@ -1732,6 +1886,9 @@ async function getLambdaFunctions() { }); return response.data; } catch (errorData) { + if (errorData.response.status === 503) { + return []; + } throw generateError(errorData); } } @@ -2427,6 +2584,19 @@ export default Object.freeze({ cancel: cancelLambdaRequest, }), + functions: Object.freeze({ + list: getFunctions, + status: getFunctionRequestStatus, + requests: getFunctionsRequests, + run: runFunctionRequest, + call: callFunction, + create: createFunction, + providers: getFunctionProviders, + delete: deleteFunction, + cancel: cancelFunctionRequest, + getPreview: getFunctionPreview, + }), + issues: Object.freeze({ create: createIssue, update: updateIssue, diff --git a/cvat-ui/package.json b/cvat-ui/package.json index d40e877e..9fb70697 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.47.1", + "version": "1.48.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 4effce33..aae77d1a 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -12,7 +12,7 @@ import { CanvasMode as Canvas3DMode } from 'cvat-canvas3d-wrapper'; import { RectDrawingMethod, CuboidDrawingMethod, Canvas, CanvasMode as Canvas2DMode, } from 'cvat-canvas-wrapper'; -import { getCore } from 'cvat-core-wrapper'; +import { getCore, MLModel } from 'cvat-core-wrapper'; import logger, { LogType } from 'cvat-logger'; import { getCVATStore } from 'cvat-store'; @@ -22,7 +22,6 @@ import { ContextMenuType, DimensionType, FrameSpeed, - Model, ObjectType, OpenCVTool, Rotation, @@ -1507,7 +1506,7 @@ export function pasteShapeAsync(): ThunkAction { }; } -export function interactWithCanvas(activeInteractor: Model | OpenCVTool, activeLabelID: number): AnyAction { +export function interactWithCanvas(activeInteractor: MLModel | OpenCVTool, activeLabelID: number): AnyAction { return { type: AnnotationActionTypes.INTERACT_WITH_CANVAS, payload: { diff --git a/cvat-ui/src/actions/cloud-storage-actions.ts b/cvat-ui/src/actions/cloud-storage-actions.ts index e8d95870..f25c6d2a 100644 --- a/cvat-ui/src/actions/cloud-storage-actions.ts +++ b/cvat-ui/src/actions/cloud-storage-actions.ts @@ -1,11 +1,13 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { Dispatch, ActionCreator } from 'redux'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { getCore } from 'cvat-core-wrapper'; -import { CloudStoragesQuery, CloudStorage, Indexable } from 'reducers'; +import { CloudStoragesQuery, CloudStorage } from 'reducers'; +import { filterNull } from 'utils/filter-null'; const cvat = getCore(); @@ -106,12 +108,7 @@ export function getCloudStoragesAsync(query: Partial): Thunk dispatch(cloudStoragesActions.getCloudStorages()); dispatch(cloudStoragesActions.updateCloudStoragesGettingQuery(query)); - const filteredQuery = { ...query }; - for (const key in filteredQuery) { - if ((filteredQuery as Indexable)[key] === null) { - delete (filteredQuery as Indexable)[key]; - } - } + const filteredQuery = filterNull(query); let result = null; try { diff --git a/cvat-ui/src/actions/jobs-actions.ts b/cvat-ui/src/actions/jobs-actions.ts index 1a2d0948..70dc5a6d 100644 --- a/cvat-ui/src/actions/jobs-actions.ts +++ b/cvat-ui/src/actions/jobs-actions.ts @@ -1,10 +1,12 @@ // Copyright (C) 2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { getCore } from 'cvat-core-wrapper'; -import { Indexable, JobsQuery, Job } from 'reducers'; +import { JobsQuery, Job } from 'reducers'; +import { filterNull } from 'utils/filter-null'; const cvat = getCore(); @@ -43,14 +45,9 @@ export type JobsActions = ActionUnion; export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch) => { try { // We remove all keys with null values from the query - const filteredQuery = { ...query }; - for (const key of Object.keys(query)) { - if ((filteredQuery as Indexable)[key] === null) { - delete (filteredQuery as Indexable)[key]; - } - } + const filteredQuery = filterNull(query); - dispatch(jobsActions.getJobs(filteredQuery)); + dispatch(jobsActions.getJobs(filteredQuery as JobsQuery)); const jobs = await cvat.jobs.get(filteredQuery); dispatch(jobsActions.getJobsSuccess(jobs)); } catch (error) { diff --git a/cvat-ui/src/actions/models-actions.ts b/cvat-ui/src/actions/models-actions.ts index dae9d451..3cdf9faa 100644 --- a/cvat-ui/src/actions/models-actions.ts +++ b/cvat-ui/src/actions/models-actions.ts @@ -1,15 +1,27 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; -import { Model, ActiveInference, RQStatus } from 'reducers'; -import { getCore } from 'cvat-core-wrapper'; +import { + ActiveInference, RQStatus, ModelsQuery, +} from 'reducers'; +import { getCore, MLModel, ModelProvider } from 'cvat-core-wrapper'; +import { filterNull } from 'utils/filter-null'; + +const cvat = getCore(); export enum ModelsActionTypes { GET_MODELS = 'GET_MODELS', GET_MODELS_SUCCESS = 'GET_MODELS_SUCCESS', GET_MODELS_FAILED = 'GET_MODELS_FAILED', + CREATE_MODEL = 'CREATE_MODEL', + CREATE_MODEL_SUCCESS = 'CREATE_MODEL_SUCCESS', + CREATE_MODEL_FAILED = 'CREATE_MODEL_FAILED', + DELETE_MODEL = 'DELETE_MODEL', + DELETE_MODEL_SUCCESS = 'DELETE_MODEL_SUCCESS', + DELETE_MODEL_FAILED = 'DELETE_MODEL_FAILED', START_INFERENCE_FAILED = 'START_INFERENCE_FAILED', GET_INFERENCE_STATUS_SUCCESS = 'GET_INFERENCE_STATUS_SUCCESS', GET_INFERENCE_STATUS_FAILED = 'GET_INFERENCE_STATUS_FAILED', @@ -18,16 +30,32 @@ export enum ModelsActionTypes { CLOSE_RUN_MODEL_DIALOG = 'CLOSE_RUN_MODEL_DIALOG', CANCEL_INFERENCE_SUCCESS = 'CANCEL_INFERENCE_SUCCESS', CANCEL_INFERENCE_FAILED = 'CANCEL_INFERENCE_FAILED', + GET_MODEL_PROVIDERS = 'GET_MODEL_PROVIDERS', + GET_MODEL_PROVIDERS_SUCCESS = 'GET_MODEL_PROVIDERS_SUCCESS', + GET_MODEL_PROVIDERS_FAILED = 'GET_MODEL_PROVIDERS_FAILED', + GET_MODEL_PREVIEW = 'GET_MODEL_PREVIEW', + GET_MODEL_PREVIEW_SUCCESS = 'GET_MODEL_PREVIEW_SUCCESS', + GET_MODEL_PREVIEW_FAILED = 'GET_MODEL_PREVIEW_FAILED', } export const modelsActions = { - getModels: () => createAction(ModelsActionTypes.GET_MODELS), - getModelsSuccess: (models: Model[]) => createAction(ModelsActionTypes.GET_MODELS_SUCCESS, { + getModels: (query?: ModelsQuery) => createAction(ModelsActionTypes.GET_MODELS, { query }), + getModelsSuccess: (models: MLModel[]) => createAction(ModelsActionTypes.GET_MODELS_SUCCESS, { models, }), getModelsFailed: (error: any) => createAction(ModelsActionTypes.GET_MODELS_FAILED, { error, }), + createModel: () => createAction(ModelsActionTypes.CREATE_MODEL), + createModelSuccess: (model: MLModel) => createAction(ModelsActionTypes.CREATE_MODEL_SUCCESS, { + model, + }), + createModelFailed: (error: any) => createAction(ModelsActionTypes.CREATE_MODEL_FAILED, { error }), + deleteModel: (model: MLModel) => createAction(ModelsActionTypes.DELETE_MODEL, { model }), + deleteModelSuccess: (modelID: string | number) => createAction(ModelsActionTypes.DELETE_MODEL_SUCCESS, { modelID }), + deleteModelFailed: (modelName: string, error: any) => ( + createAction(ModelsActionTypes.DELETE_MODEL_FAILED, { modelName, error }) + ), fetchMetaFailed: (error: any) => createAction(ModelsActionTypes.FETCH_META_FAILED, { error }), getInferenceStatusSuccess: (taskID: number, activeInference: ActiveInference) => ( createAction(ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS, { @@ -64,18 +92,34 @@ export const modelsActions = { taskInstance, }) ), + getModelProviders: () => createAction(ModelsActionTypes.GET_MODEL_PROVIDERS), + getModelProvidersSuccess: (providers: ModelProvider[]) => ( + createAction(ModelsActionTypes.GET_MODEL_PROVIDERS_SUCCESS, { + providers, + })), + getModelProvidersFailed: (error: any) => createAction(ModelsActionTypes.GET_MODEL_PROVIDERS_FAILED, { error }), + getModelPreview: (modelID: string | number) => ( + createAction(ModelsActionTypes.GET_MODEL_PREVIEW, { modelID }) + ), + getModelPreviewSuccess: (modelID: string | number, preview: string) => ( + createAction(ModelsActionTypes.GET_MODEL_PREVIEW_SUCCESS, { modelID, preview }) + ), + getModelPreviewFailed: (modelID: string | number, error: any) => ( + createAction(ModelsActionTypes.GET_MODEL_PREVIEW_FAILED, { modelID, error }) + ), }; export type ModelsActions = ActionUnion; const core = getCore(); -export function getModelsAsync(): ThunkAction { +export function getModelsAsync(query: ModelsQuery): ThunkAction { return async (dispatch): Promise => { - dispatch(modelsActions.getModels()); + dispatch(modelsActions.getModels(query)); + const filteredQuery = filterNull(query); try { - const models = await core.lambda.list(); + const models = await core.lambda.list(filteredQuery); dispatch(modelsActions.getModelsSuccess(models)); } catch (error) { dispatch(modelsActions.getModelsFailed(error)); @@ -83,15 +127,43 @@ export function getModelsAsync(): ThunkAction { }; } +export function createModelAsync(modelData: Record): ThunkAction { + return async function (dispatch) { + const model = new cvat.classes.MLModel(modelData); + + dispatch(modelsActions.createModel()); + try { + const createdModel = await model.save(); + dispatch(modelsActions.createModelSuccess(createdModel)); + } catch (error) { + dispatch(modelsActions.createModelFailed(error)); + throw error; + } + }; +} + +export function deleteModelAsync(model: MLModel): ThunkAction { + return async function (dispatch) { + dispatch(modelsActions.deleteModel(model)); + try { + await model.delete(); + dispatch(modelsActions.deleteModelSuccess(model.id)); + } catch (error) { + dispatch(modelsActions.deleteModelFailed(model.name, error)); + } + }; +} + interface InferenceMeta { taskID: number; requestID: string; + functionID: string | number; } function listen(inferenceMeta: InferenceMeta, dispatch: (action: ModelsActions) => void): void { - const { taskID, requestID } = inferenceMeta; + const { taskID, requestID, functionID } = inferenceMeta; core.lambda - .listen(requestID, (status: RQStatus, progress: number, message: string) => { + .listen(requestID, functionID, (status: RQStatus, progress: number, message: string) => { if (status === RQStatus.failed || status === RQStatus.unknown) { dispatch( modelsActions.getInferenceStatusFailed( @@ -107,6 +179,7 @@ function listen(inferenceMeta: InferenceMeta, dispatch: (action: ModelsActions) modelsActions.getInferenceStatusSuccess(taskID, { status, progress, + functionID, error: message, id: requestID, }), @@ -119,6 +192,7 @@ function listen(inferenceMeta: InferenceMeta, dispatch: (action: ModelsActions) progress: 0, error: error.toString(), id: requestID, + functionID, }), ); }); @@ -136,6 +210,7 @@ export function getInferenceStatusAsync(): ThunkAction { .map((request: any): object => ({ taskID: +request.function.task, requestID: request.id, + functionID: request.function.id, })) .forEach((inferenceMeta: InferenceMeta): void => { listen(inferenceMeta, dispatchCallback); @@ -146,7 +221,7 @@ export function getInferenceStatusAsync(): ThunkAction { }; } -export function startInferenceAsync(taskId: number, model: Model, body: object): ThunkAction { +export function startInferenceAsync(taskId: number, model: MLModel, body: object): ThunkAction { return async (dispatch): Promise => { try { const requestID: string = await core.lambda.run(taskId, model, body); @@ -157,6 +232,7 @@ export function startInferenceAsync(taskId: number, model: Model, body: object): listen( { taskID: taskId, + functionID: model.id, requestID, }, dispatchCallback, @@ -171,10 +247,32 @@ export function cancelInferenceAsync(taskID: number): ThunkAction { return async (dispatch, getState): Promise => { try { const inference = getState().models.inferences[taskID]; - await core.lambda.cancel(inference.id); + await core.lambda.cancel(inference.id, inference.functionID); dispatch(modelsActions.cancelInferenceSuccess(taskID)); } catch (error) { dispatch(modelsActions.cancelInferenceFailed(taskID, error)); } }; } + +export function getModelProvidersAsync(): ThunkAction { + return async function (dispatch) { + dispatch(modelsActions.getModelProviders()); + try { + const providers = await cvat.lambda.providers(); + dispatch(modelsActions.getModelProvidersSuccess(providers)); + } catch (error) { + dispatch(modelsActions.getModelProvidersFailed(error)); + } + }; +} + +export const getModelPreviewAsync = (model: MLModel): ThunkAction => async (dispatch) => { + dispatch(modelsActions.getModelPreview(model.id)); + try { + const result = await model.getPreview(); + dispatch(modelsActions.getModelPreviewSuccess(model.id, result)); + } catch (error) { + dispatch(modelsActions.getModelPreviewFailed(model.id, error)); + } +}; diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index d07fecbd..b62523e6 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,11 +7,12 @@ import { Dispatch, ActionCreator } from 'redux'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { - ProjectsQuery, TasksQuery, CombinedState, Indexable, + ProjectsQuery, TasksQuery, CombinedState, } from 'reducers'; import { getTasksAsync } from 'actions/tasks-actions'; import { getCVATStore } from 'cvat-store'; import { getCore } from 'cvat-core-wrapper'; +import { filterNull } from 'utils/filter-null'; const cvat = getCore(); @@ -99,17 +100,10 @@ export function getProjectsAsync( dispatch(projectActions.updateProjectsGettingQuery(query, tasksQuery)); // Clear query object from null fields - const filteredQuery: Partial = { + const filteredQuery: Partial = filterNull({ page: 1, ...query, - }; - - for (const key of Object.keys(filteredQuery)) { - const value = (filteredQuery as Indexable)[key]; - if (value === null || typeof value === 'undefined') { - delete (filteredQuery as Indexable)[key]; - } - } + }); let result = null; try { diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 8b0cecf9..ee6d8d80 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -1,14 +1,15 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; import { - TasksQuery, CombinedState, Indexable, StorageLocation, + TasksQuery, CombinedState, StorageLocation, } from 'reducers'; import { getCore, Storage } from 'cvat-core-wrapper'; +import { filterNull } from 'utils/filter-null'; import { getInferenceStatusAsync } from './models-actions'; const cvat = getCore(); @@ -74,13 +75,7 @@ export function getTasksAsync( return async (dispatch: ActionCreator): Promise => { dispatch(getTasks(query, updateQuery)); - // We remove all keys with null values from the query - const filteredQuery = { ...query }; - for (const key of Object.keys(query)) { - if ((filteredQuery as Indexable)[key] === null) { - delete (filteredQuery as Indexable)[key]; - } - } + const filteredQuery = filterNull(query); let result = null; try { diff --git a/cvat-ui/src/actions/webhooks-actions.ts b/cvat-ui/src/actions/webhooks-actions.ts index 1b20b1ee..193b6ce0 100644 --- a/cvat-ui/src/actions/webhooks-actions.ts +++ b/cvat-ui/src/actions/webhooks-actions.ts @@ -1,11 +1,12 @@ -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { getCore, Webhook } from 'cvat-core-wrapper'; import { Dispatch, ActionCreator, Store } from 'redux'; -import { Indexable, WebhooksQuery } from 'reducers'; +import { WebhooksQuery } from 'reducers'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; +import { filterNull } from 'utils/filter-null'; const cvat = getCore(); @@ -47,13 +48,7 @@ export const getWebhooksAsync = (query: WebhooksQuery): ThunkAction => ( async (dispatch: ActionCreator): Promise => { dispatch(webhooksActions.getWebhooks(query)); - // We remove all keys with null values from the query - const filteredQuery = { ...query }; - for (const key of Object.keys(query)) { - if ((filteredQuery as Indexable)[key] === null) { - delete (filteredQuery as Indexable)[key]; - } - } + const filteredQuery = filterNull(query); let result = null; try { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx index 7b9a4509..7faaa5f2 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -26,10 +27,12 @@ import lodash from 'lodash'; import { AIToolsIcon } from 'icons'; import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper'; -import { getCore, Attribute, Label } from 'cvat-core-wrapper'; +import { + getCore, Attribute, Label, MLModel, +} from 'cvat-core-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import { - CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState, ModelAttribute, + CombinedState, ActiveControl, ObjectType, ShapeType, ToolsBlockerState, ModelAttribute, } from 'reducers'; import { interactWithCanvas, @@ -57,9 +60,9 @@ interface StateToProps { jobInstance: any; isActivated: boolean; frame: number; - interactors: Model[]; - detectors: Model[]; - trackers: Model[]; + interactors: MLModel[]; + detectors: MLModel[]; + trackers: MLModel[]; curZOrder: number; defaultApproxPolyAccuracy: number; toolsBlockerState: ToolsBlockerState; @@ -67,7 +70,7 @@ interface StateToProps { } interface DispatchToProps { - onInteractionStart(activeInteractor: Model, activeLabelID: number): void; + onInteractionStart(activeInteractor: MLModel, activeLabelID: number): void; updateAnnotations(statesToUpdate: any[]): void; createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void; fetchAnnotations(): void; @@ -133,13 +136,13 @@ interface TrackedShape { clientID: number; serverlessState: any; shapePoints: number[]; - trackerModel: Model; + trackerModel: MLModel; } interface State { - activeInteractor: Model | null; + activeInteractor: MLModel | null; activeLabelID: number; - activeTracker: Model | null; + activeTracker: MLModel | null; convertMasksToPolygons: boolean; trackedShapes: TrackedShape[]; fetching: boolean; @@ -211,7 +214,7 @@ export class ToolsControlComponent extends React.PureComponent { }; lastestApproximatedPoints: number[][]; latestRequest: null | { - interactor: Model; + interactor: MLModel; data: { frame: number; neg_points: number[][]; @@ -444,7 +447,7 @@ export class ToolsControlComponent extends React.PureComponent { this.constructFromPoints(this.interaction.lastestApproximatedPoints); } } else if (shapesUpdated) { - const interactor = activeInteractor as Model; + const interactor = activeInteractor as MLModel; this.interaction.latestRequest = { interactor, data: { @@ -498,7 +501,7 @@ export class ToolsControlComponent extends React.PureComponent { clientID, serverlessState: null, shapePoints: points, - trackerModel: activeTracker as Model, + trackerModel: activeTracker as MLModel, }, ], }); @@ -527,7 +530,7 @@ export class ToolsControlComponent extends React.PureComponent { private setActiveInteractor = (value: string): void => { const { interactors } = this.props; - const [interactor] = interactors.filter((_interactor: Model) => _interactor.id === value); + const [interactor] = interactors.filter((_interactor: MLModel) => _interactor.id === value); if (interactor.version < MIN_SUPPORTED_INTERACTOR_VERSION) { notification.warning({ @@ -544,7 +547,7 @@ export class ToolsControlComponent extends React.PureComponent { private setActiveTracker = (value: string): void => { const { trackers } = this.props; this.setState({ - activeTracker: trackers.filter((tracker: Model) => tracker.id === value)[0], + activeTracker: trackers.filter((tracker: MLModel) => tracker.id === value)[0], }); }; @@ -723,7 +726,7 @@ export class ToolsControlComponent extends React.PureComponent { for (const trackerID of Object.keys(trackingData.stateless)) { let hideMessage = null; try { - const [tracker] = trackers.filter((_tracker: Model) => _tracker.id === trackerID); + const [tracker] = trackers.filter((_tracker: MLModel) => _tracker.id === trackerID); if (!tracker) { throw new Error(`Suitable tracker with ID ${trackerID} not found in tracker list`); } @@ -770,7 +773,7 @@ export class ToolsControlComponent extends React.PureComponent { // 4. run tracking for all the objects let hideMessage = null; try { - const [tracker] = trackers.filter((_tracker: Model) => _tracker.id === trackerID); + const [tracker] = trackers.filter((_tracker: MLModel) => _tracker.id === trackerID); if (!tracker) { throw new Error(`Suitable tracker with ID ${trackerID} not found in tracker list`); } @@ -955,7 +958,7 @@ export class ToolsControlComponent extends React.PureComponent { onChange={this.setActiveTracker} > {trackers.map( - (tracker: Model): JSX.Element => ( + (tracker: MLModel): JSX.Element => ( {tracker.name} @@ -1030,7 +1033,7 @@ export class ToolsControlComponent extends React.PureComponent { onChange={this.setActiveInteractor} > {interactors.map( - (interactor: Model): JSX.Element => ( + (interactor: MLModel): JSX.Element => ( { models={detectors} labels={jobInstance.labels} dimension={jobInstance.dimension} - runInference={async (model: Model, body: DetectorRequestBody) => { + runInference={async (model: MLModel, body: DetectorRequestBody) => { try { this.setState({ mode: 'detection', fetching: true }); const result = await core.lambda.call(jobInstance.taskId, model, { diff --git a/cvat-ui/src/components/common/preview.tsx b/cvat-ui/src/components/common/preview.tsx index 0faf0a63..b96066b7 100644 --- a/cvat-ui/src/components/common/preview.tsx +++ b/cvat-ui/src/components/common/preview.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -14,12 +14,15 @@ import { getCloudStoragePreviewAsync } from 'actions/cloud-storage-actions'; import { CombinedState, Job, Task, Project, CloudStorage, } from 'reducers'; +import MLModel from 'cvat-core/src/ml-model'; +import { getModelPreviewAsync } from 'actions/models-actions'; interface Props { job?: Job | undefined; task?: Task | undefined; project?: Project | undefined; cloudStorage?: CloudStorage | undefined; + model?: MLModel | undefined; onClick?: (event: React.MouseEvent) => void; loadingClassName?: string; emptyPreviewClassName?: string; @@ -35,6 +38,7 @@ export default function Preview(props: Props): JSX.Element { task, project, cloudStorage, + model, onClick, loadingClassName, emptyPreviewClassName, @@ -51,6 +55,8 @@ export default function Preview(props: Props): JSX.Element { return state.tasks.previews[task.id]; } if (cloudStorage !== undefined) { return state.cloudStorages.previews[cloudStorage.id]; + } if (model !== undefined) { + return state.models.previews[model.id]; } return ''; }); @@ -65,6 +71,8 @@ export default function Preview(props: Props): JSX.Element { dispatch(getTaskPreviewAsync(task)); } else if (cloudStorage !== undefined) { dispatch(getCloudStoragePreviewAsync(cloudStorage)); + } else if (model !== undefined) { + dispatch(getModelPreviewAsync(model)); } } }, [preview]); @@ -79,7 +87,7 @@ export default function Preview(props: Props): JSX.Element { if (preview.initialized && !preview.preview) { return ( -
+
); 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 00000000..aee1c6da --- /dev/null +++ b/cvat-ui/src/components/create-model-page/create-model-page.tsx @@ -0,0 +1,48 @@ +// Copyright (C) 2022-2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React, { useEffect } from 'react'; +import { Row, Col } from 'antd/lib/grid'; +import Text from 'antd/lib/typography/Text'; +import Spin from 'antd/lib/spin'; +import { CombinedState } from 'reducers'; +import { useSelector, useDispatch } from 'react-redux'; +import { getModelProvidersAsync } from 'actions/models-actions'; +import ModelForm from './model-form'; + +function CreateModelPage(): JSX.Element { + const dispatch = useDispatch(); + const fetching = useSelector((state: CombinedState) => state.models.providers.fetching); + const providers = useSelector((state: CombinedState) => state.models.providers.list); + useEffect(() => { + dispatch(getModelProvidersAsync()); + }, []); + + return ( +
+ + + Add a model + + + { + fetching ? ( +
+ +
+ ) : ( + + + + + + ) + } +
+ ); +} + +export default React.memo(CreateModelPage); diff --git a/cvat-ui/src/components/create-model-page/model-form.tsx b/cvat-ui/src/components/create-model-page/model-form.tsx new file mode 100644 index 00000000..7eaa6f5d --- /dev/null +++ b/cvat-ui/src/components/create-model-page/model-form.tsx @@ -0,0 +1,162 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React, { useCallback, useState } from 'react'; +import { Store } from 'antd/lib/form/interface'; +import { Row, Col } from 'antd/lib/grid'; +import Form from 'antd/lib/form'; +import Button from 'antd/lib/button'; +import Select from 'antd/lib/select'; +import notification from 'antd/lib/notification'; +import Input from 'antd/lib/input/Input'; + +import { CombinedState } from 'reducers'; +import { useHistory } from 'react-router'; +import { useDispatch, useSelector } from 'react-redux'; +import { createModelAsync } from 'actions/models-actions'; +import { ModelProvider } from 'cvat-core-wrapper'; +import ModelProviderIcon from 'components/models-page/model-provider-icon'; + +interface Props { + providers: ModelProvider[]; +} + +function createProviderFormItems(providerAttributes: Record): JSX.Element { + delete providerAttributes.url; + return ( + <> + { + Object.entries(providerAttributes).map(([key, text]) => ( + + + + )) + } + + ); +} + +function ModelForm(props: Props): JSX.Element { + const { providers } = props; + const providerList = providers.map((provider) => ({ + value: provider.name, + text: provider.name.charAt(0).toUpperCase() + provider.name.slice(1), + })); + const providerMap = Object.fromEntries(providers.map((provider) => [provider.name, provider.attributes])); + + const [form] = Form.useForm(); + const history = useHistory(); + const dispatch = useDispatch(); + const fetching = useSelector((state: CombinedState) => state.models.fetching); + const [currentProviderForm, setCurrentProviderForm] = useState(null); + const onChangeProviderValue = useCallback((provider: string) => { + setCurrentProviderForm(createProviderFormItems(providerMap[provider])); + const emptiedKeys: Record = { ...providerMap[provider] }; + Object.keys(providerMap[provider]).forEach((k) => { emptiedKeys[k] = null; }); + form.setFieldsValue(emptiedKeys); + }, []); + const [providerTouched, setProviderTouched] = useState(false); + const [currentUrlEmpty, setCurrentUrlEmpty] = useState(true); + + const handleSubmit = useCallback(async (): Promise => { + try { + const values: Store = await form.validateFields(); + await dispatch(createModelAsync(values)); + form.resetFields(); + setCurrentProviderForm(null); + setProviderTouched(false); + setCurrentUrlEmpty(true); + notification.info({ + message: 'Model has been successfully created', + className: 'cvat-notification-create-model-success', + }); + // eslint-disable-next-line no-empty + } catch (e) {} + }, []); + + return ( + + +
+ + + ) => { + const { value } = event.target; + const guessedProvider = providers.find((provider) => value.includes(provider.name)); + if (guessedProvider && !providerTouched) { + form.setFieldsValue({ provider: guessedProvider.name }); + setCurrentProviderForm(createProviderFormItems(providerMap[guessedProvider.name])); + } + setCurrentUrlEmpty(!value); + }} + /> + + + { + !currentUrlEmpty && ( + <> + + + + {currentProviderForm} + + ) + } +
+ + + + + + + + + + + +
+ ); +} + +export default React.memo(ModelForm); diff --git a/cvat-ui/src/components/create-model-page/styles.scss b/cvat-ui/src/components/create-model-page/styles.scss new file mode 100644 index 00000000..7c1a31b6 --- /dev/null +++ b/cvat-ui/src/components/create-model-page/styles.scss @@ -0,0 +1,46 @@ +// Copyright (C) 2022-2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-create-model-page { + width: 100%; + height: 100%; + padding-top: $grid-unit-size * 5; + + .cvat-title { + font-size: 36px; + } +} + +.cvat-create-model-form-wrapper { + margin-top: $grid-unit-size * 3; + height: auto; + border: 1px solid $border-color-1; + border-radius: 3px; + padding: $grid-unit-size * 3; + background: $background-color-1; + text-align: initial; +} + +.cvat-create-models-actions { + margin-top: $grid-unit-size * 2; +} + +.cvat-model-provider-icon { + display: flex; + + img { + margin-top: 3px; + margin-right: $grid-unit-size; + width: $grid-unit-size * 2; + height: $grid-unit-size * 2; + } +} + +.cvat-select-model-provider { + img { + margin-top: 7px; + } +} diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 061200ae..93c41e36 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -33,9 +33,9 @@ import ExportDatasetModal from 'components/export-dataset/export-dataset-modal'; import ExportBackupModal from 'components/export-backup/export-backup-modal'; import ImportDatasetModal from 'components/import-dataset/import-dataset-modal'; import ImportBackupModal from 'components/import-backup/import-backup-modal'; -import ModelsPageContainer from 'containers/models-page/models-page'; import JobsPageComponent from 'components/jobs-page/jobs-page'; +import ModelsPageComponent from 'components/models-page/models-page'; import TasksPageContainer from 'containers/tasks-page/tasks-page'; import CreateTaskPageContainer from 'containers/create-task-page/create-task-page'; @@ -72,6 +72,7 @@ import appConfig from 'config'; import EmailConfirmationPage from './email-confirmation-pages/email-confirmed'; import EmailVerificationSentPage from './email-confirmation-pages/email-verification-sent'; import IncorrectEmailConfirmationPage from './email-confirmation-pages/incorrect-email-confirmation'; +import CreateModelPage from './create-model-page/create-model-page'; interface CVATAppProps { loadFormats: () => void; @@ -330,7 +331,7 @@ class CVATApplication extends React.PureComponent{title} ), duration: null, - description: error.length > 200 ? 'Open the Browser Console to get details' : {error}, + description: error.length > 300 ? 'Open the Browser Console to get details' : {error}, }); // eslint-disable-next-line no-console @@ -449,7 +450,14 @@ class CVATApplication extends React.PureComponent {isModelPluginActive && ( - + + + + + + )} >({}); const model = models.filter((_model): boolean => _model.id === modelID)[0]; - const isDetector = model && model.type === 'detector'; - const isReId = model && model.type === 'reid'; + const isDetector = model && model.kind === ModelKind.DETECTOR; + const isReId = model && model.kind === ModelKind.REID; + const isClassifier = model && model.kind === ModelKind.CLASSIFIER; + const convertMasksToPolygonsAvailable = isDetector && + (!model.returnType || model.returnType === ModelReturnType.MASK); const buttonEnabled = - model && (model.type === 'reid' || (model.type === 'detector' && !!Object.keys(mapping).length)); - - const modelLabels = (isDetector ? model.labels : []).filter((_label: string): boolean => !(_label in mapping)); - const taskLabels = isDetector ? labels.map((label: any): string => label.name) : []; - - if (model && model.type !== 'reid' && !model.labels.length) { + model && (model.kind === ModelKind.REID || + (model.kind === ModelKind.DETECTOR && !!Object.keys(mapping).length) || + (model.kind === ModelKind.CLASSIFIER && !!Object.keys(mapping).length)); + const canHaveMapping = isDetector || isClassifier; + const modelLabels = (canHaveMapping ? model.labels : []).filter((_label: string): boolean => !(_label in mapping)); + const taskLabels = canHaveMapping ? labels.map((label: any): string => label.name) : []; + + if (model && model.kind === ModelKind.REID && !model.labels.length) { notification.warning({ message: 'The selected model does not include any labels', }); @@ -241,7 +248,6 @@ function DetectorRunner(props: Props): JSX.Element { return acc; }, {}, ); - setMapping(defaultMapping); setMatch({ model: null, task: null }); setAttrMatch({}); @@ -249,7 +255,7 @@ function DetectorRunner(props: Props): JSX.Element { }} > {models.map( - (_model: Model): JSX.Element => ( + (_model: MLModel): JSX.Element => ( {_model.name} @@ -258,7 +264,7 @@ function DetectorRunner(props: Props): JSX.Element { - {isDetector && + {canHaveMapping && Object.keys(mapping).length ? Object.keys(mapping).map((modelLabel: string) => { const label = labels @@ -337,7 +343,7 @@ function DetectorRunner(props: Props): JSX.Element { ); }) : null} - {isDetector && !!taskLabels.length && !!modelLabels.length ? ( + {canHaveMapping && !!taskLabels.length && !!modelLabels.length ? ( <> @@ -354,7 +360,7 @@ function DetectorRunner(props: Props): JSX.Element { ) : null} - {isDetector && ( + {convertMasksToPolygonsAvailable && (
{ - const detectorRequestBody: DetectorRequestBody = { - mapping, - cleanup, - convMaskToPoly: convertMasksToPolygons, - }; - - runInference( - model, - model.type === 'detector' ? detectorRequestBody : { + let requestBody: object = {}; + if (model.kind === ModelKind.DETECTOR) { + requestBody = { + mapping, + cleanup, + convMaskToPoly: convertMasksToPolygons, + }; + } else if (model.kind === ModelKind.REID) { + requestBody = { threshold, max_distance: distance, - }, - ); + }; + } else if (model.kind === ModelKind.CLASSIFIER) { + requestBody = { + mapping, + }; + } + + runInference(model, requestBody); }} > Annotate diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx index c8b4f333..3ef081c9 100644 --- a/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx +++ b/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -9,36 +10,39 @@ import Modal from 'antd/lib/modal'; import { ThunkDispatch } from 'utils/redux'; import { modelsActions, startInferenceAsync } from 'actions/models-actions'; -import { Model, CombinedState } from 'reducers'; +import { CombinedState } from 'reducers'; +import MLModel from 'cvat-core/src/ml-model'; import DetectorRunner from './detector-runner'; interface StateToProps { visible: boolean; task: any; - detectors: Model[]; - reid: Model[]; + detectors: MLModel[]; + reid: MLModel[]; + classifiers: MLModel[]; } interface DispatchToProps { - runInference(task: any, model: Model, body: object): void; + runInference(task: any, model: MLModel, body: object): void; closeDialog(): void; } function mapStateToProps(state: CombinedState): StateToProps { const { models } = state; - const { detectors, reid } = models; + const { detectors, reid, classifiers } = models; return { visible: models.modelRunnerIsVisible, task: models.modelRunnerTask, reid, detectors, + classifiers, }; } function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { return { - runInference(taskID: number, model: Model, body: object) { + runInference(taskID: number, model: MLModel, body: object) { dispatch(startInferenceAsync(taskID, model, body)); }, closeDialog() { @@ -49,10 +53,10 @@ function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { function ModelRunnerDialog(props: StateToProps & DispatchToProps): JSX.Element { const { - reid, detectors, task, visible, runInference, closeDialog, + reid, detectors, classifiers, task, visible, runInference, closeDialog, } = props; - const models = [...reid, ...detectors]; + const models = [...reid, ...detectors, ...classifiers]; return ( { + setIsModalShown(true); + }; + const onCloseModel = () => { + setIsModalShown(false); + }; + + const onDelete = useCallback(() => { + setIsRemoved(true); + }, []); + + const created = moment(model.createdDate).fromNow(); + const icon = ; return ( - - - {model.framework} - - - - {model.name} - - - - {model.type} - - - - {model.description} - - - - - - + /> + { + icon ?
{icon}
: null + } + + ); } diff --git a/cvat-ui/src/components/models-page/deployed-models-list.tsx b/cvat-ui/src/components/models-page/deployed-models-list.tsx index c8cb6c51..1caf9d40 100644 --- a/cvat-ui/src/components/models-page/deployed-models-list.tsx +++ b/cvat-ui/src/components/models-page/deployed-models-list.tsx @@ -1,44 +1,35 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React from 'react'; +import moment from 'moment'; +import { useSelector } from 'react-redux'; import { Row, Col } from 'antd/lib/grid'; -import Text from 'antd/lib/typography/Text'; - -import { Model } from 'reducers'; +import { CombinedState } from 'reducers'; +import { MLModel } from 'cvat-core-wrapper'; +import { ModelProviders } from 'cvat-core/src/enums'; import DeployedModelItem from './deployed-model-item'; -interface Props { - models: Model[]; -} - -export default function DeployedModelsListComponent(props: Props): JSX.Element { - const { models } = props; +export default function DeployedModelsListComponent(): JSX.Element { + const interactors = useSelector((state: CombinedState) => state.models.interactors); + const detectors = useSelector((state: CombinedState) => state.models.detectors); + const trackers = useSelector((state: CombinedState) => state.models.trackers); + const reid = useSelector((state: CombinedState) => state.models.reid); + const classifiers = useSelector((state: CombinedState) => state.models.classifiers); + const models = [...interactors, ...detectors, ...trackers, ...reid, ...classifiers]; + const builtInModels = models.filter((model: MLModel) => model.provider === ModelProviders.CVAT); + const externalModels = models.filter((model: MLModel) => model.provider !== ModelProviders.CVAT); + externalModels.sort((a, b) => moment(a.createdDate).valueOf() - moment(b.createdDate).valueOf()); - const items = models.map((model): JSX.Element => ); + const renderModels = [...builtInModels, ...externalModels]; + const items = renderModels.map((model): JSX.Element => ); return ( <> - - - - Framework - - - Name - - - Type - - - Description - - - Labels - - + {items} diff --git a/cvat-ui/src/components/models-page/empty-list.tsx b/cvat-ui/src/components/models-page/empty-list.tsx index c5b246fa..4ea1d57a 100644 --- a/cvat-ui/src/components/models-page/empty-list.tsx +++ b/cvat-ui/src/components/models-page/empty-list.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-ui/src/components/models-page/model-provider-icon.tsx b/cvat-ui/src/components/models-page/model-provider-icon.tsx new file mode 100644 index 00000000..49bb0443 --- /dev/null +++ b/cvat-ui/src/components/models-page/model-provider-icon.tsx @@ -0,0 +1,29 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { ModelProvider } from 'cvat-core-wrapper'; +import { CombinedState } from 'reducers'; +import { useSelector } from 'react-redux'; + +interface Props { + providerName: string; +} + +export default function ModelProviderIcon(props: Props): JSX.Element | null { + const { providerName } = props; + const providers = useSelector((state: CombinedState) => state.models.providers.list); + + let icon: JSX.Element | null = null; + const providerInstance = providers.find((_provider: ModelProvider) => _provider.name === providerName); + if (providerInstance) { + icon = ( + {providerName} + ); + } + return icon; +} diff --git a/cvat-ui/src/components/models-page/models-action-menu.tsx b/cvat-ui/src/components/models-page/models-action-menu.tsx new file mode 100644 index 00000000..c2b63b1b --- /dev/null +++ b/cvat-ui/src/components/models-page/models-action-menu.tsx @@ -0,0 +1,66 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'antd/lib/modal'; +import Menu from 'antd/lib/menu'; +import { MLModel, ModelProviders } from 'cvat-core-wrapper'; +import { deleteModelAsync } from 'actions/models-actions'; + +interface Props { + model: MLModel; + onDelete: () => void; +} + +export default function ModelActionsMenuComponent(props: Props): JSX.Element { + const { model, onDelete } = props; + const { provider } = model; + const cvatProvider = provider === ModelProviders.CVAT; + + const dispatch = useDispatch(); + + const onDeleteModel = useCallback((): void => { + Modal.confirm({ + title: `The model ${model.name} will be deleted`, + content: 'You will not be able to use it anymore. Continue?', + className: 'cvat-modal-confirm-remove-model', + onOk: () => { + dispatch(deleteModelAsync(model)); + onDelete(); + }, + okButtonProps: { + type: 'primary', + danger: true, + }, + okText: 'Delete', + }); + }, []); + + const onOpenUrl = useCallback((): void => { + window.open(model.url, '_blank'); + }, []); + + return ( + + { + !cvatProvider && ( + + Open model URL + + ) + } + { + !cvatProvider && ( + <> + + + Delete + + + ) + } + + ); +} diff --git a/cvat-ui/src/components/models-page/models-filter-configuration.tsx b/cvat-ui/src/components/models-page/models-filter-configuration.tsx new file mode 100644 index 00000000..e753bad0 --- /dev/null +++ b/cvat-ui/src/components/models-page/models-filter-configuration.tsx @@ -0,0 +1,55 @@ +// Copyright (C) 2022-2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { Config } from 'react-awesome-query-builder'; + +export const config: Partial = { + fields: { + description: { + label: 'Description', + type: 'text', + valueSources: ['value'], + operators: ['like'], + }, + target_url: { + label: 'Target URL', + type: 'text', + valueSources: ['value'], + operators: ['like'], + }, + owner: { + label: 'Owner', + type: 'text', + valueSources: ['value'], + operators: ['equal'], + }, + updated_date: { + label: 'Last updated', + type: 'datetime', + operators: ['between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'], + }, + type: { + label: 'Type', + type: 'select', + valueSources: ['value'], + fieldSettings: { + listValues: [ + { value: 'organization', title: 'Organization' }, + { value: 'project', title: 'Project' }, + ], + }, + }, + id: { + label: 'ID', + type: 'number', + operators: ['equal', 'between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'], + fieldSettings: { min: 0 }, + valueSources: ['value'], + }, + }, +}; + +export const localStorageRecentCapacity = 10; +export const localStorageRecentKeyword = 'recentlyAppliedWebhooksFilters'; +export const predefinedFilterValues = {}; diff --git a/cvat-ui/src/components/models-page/models-page.tsx b/cvat-ui/src/components/models-page/models-page.tsx index f6b05ba0..bb0ef2f5 100644 --- a/cvat-ui/src/components/models-page/models-page.tsx +++ b/cvat-ui/src/components/models-page/models-page.tsx @@ -1,33 +1,91 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import './styles.scss'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router'; +import { useDispatch, useSelector } from 'react-redux'; +import { getModelProvidersAsync, getModelsAsync } from 'actions/models-actions'; +import { updateHistoryFromQuery } from 'components/resource-sorting-filtering'; +import Spin from 'antd/lib/spin'; import DeployedModelsList from './deployed-models-list'; import EmptyListComponent from './empty-list'; import FeedbackComponent from '../feedback/feedback'; -import { Model } from '../../reducers'; +import { CombinedState } from '../../reducers'; +import TopBar from './top-bar'; -interface Props { - interactors: Model[]; - detectors: Model[]; - trackers: Model[]; - reid: Model[]; -} +function ModelsPageComponent(): JSX.Element { + const history = useHistory(); + const dispatch = useDispatch(); + const fetching = useSelector((state: CombinedState) => state.models.fetching); + const query = useSelector((state: CombinedState) => state.models.query); + const totalCount = useSelector((state: CombinedState) => state.models.totalCount); + + const onCreateModel = useCallback(() => { + history.push('/models/create'); + }, []); -export default function ModelsPageComponent(props: Props): JSX.Element { - const { - interactors, detectors, trackers, reid, - } = props; + const updatedQuery = { ...query }; + useEffect(() => { + history.replace({ + search: updateHistoryFromQuery(query), + }); + }, [query]); - const deployedModels = [...detectors, ...interactors, ...trackers, ...reid]; + useEffect(() => { + dispatch(getModelProvidersAsync()); + dispatch(getModelsAsync()); + }, []); + + const content = totalCount ? ( + + ) : ; return (
- {deployedModels.length ? : } + { + dispatch( + getModelsAsync({ + ...query, + search, + page: 1, + }), + ); + }} + onApplyFilter={(filter: string | null) => { + dispatch( + getModelsAsync({ + ...query, + filter, + page: 1, + }), + ); + }} + onApplySorting={(sorting: string | null) => { + dispatch( + getModelsAsync({ + ...query, + sort: sorting, + page: 1, + }), + ); + }} + /> + { fetching ? ( +
+ +
+ ) : content }
); } + +export default React.memo(ModelsPageComponent); diff --git a/cvat-ui/src/components/models-page/styles.scss b/cvat-ui/src/components/models-page/styles.scss index 13237e07..b84d71cb 100644 --- a/cvat-ui/src/components/models-page/styles.scss +++ b/cvat-ui/src/components/models-page/styles.scss @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,14 +8,10 @@ .cvat-models-page { padding-top: $grid-unit-size * 2; padding-bottom: $grid-unit-size; - height: 90%; overflow: auto; position: fixed; + height: 100%; width: 100%; - - > div:nth-child(1) { - margin-bottom: $grid-unit-size; - } } .cvat-empty-models-list { @@ -26,10 +23,12 @@ .cvat-models-list { height: 100%; - overflow-y: auto; + display: flex; + flex-wrap: wrap; } .cvat-models-list-item { + position: relative; width: 100%; height: auto; border: 1px solid $border-color-1; @@ -59,3 +58,177 @@ overflow: hidden; } } + +.cvat-models-item-card-removed { + opacity: 0.5; + pointer-events: none; +} + +.cvat-models-page-top-bar { + margin: $grid-unit-size * 3 0; + + > div { + display: flex; + } +} + +.cvat-models-heading { + padding: $grid-unit-size * 2; +} + +.cvat-models-page-filters-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + + > div { + display: flex; + margin-right: $grid-unit-size * 4; + + > button { + margin-right: $grid-unit-size; + } + } +} + +.cvat-models-add-wrapper { + display: inline-block; +} + +.cvat-model-delete { + position: absolute; + top: $grid-unit-size; + right: $grid-unit-size; + color: #8c8c8c; + font-size: 10px; + + &:hover { + cursor: pointer; + color: #595959; + } +} + +.cvat-models-item-card { + width: 25%; + border-width: 4px; + height: $grid-unit-size * 28; + overflow: hidden; + + .ant-card-meta-title { + margin-bottom: 0 !important; + } +} + +.cvat-model-item-loading-preview, +.cvat-model-item-empty-preview { + .ant-spin { + position: inherit; + } + + font-size: 80px; + text-align: center; + height: $grid-unit-size * 18; + + &:hover { + cursor: pointer; + } +} + +.cvat-models-item-card-preview-wrapper { + display: flex; + justify-content: center; + align-items: center; + height: $grid-unit-size * 18; + overflow: hidden; + + &:hover { + cursor: pointer; + } +} + +.cvat-model-item-provider { + position: absolute; + top: $grid-unit-size; + right: $grid-unit-size; +} + +.cvat-model-item-provider-inner { + @extend .cvat-model-item-provider; + + right: $grid-unit-size * 2; + + svg { + width: $grid-unit-size * 4; + height: $grid-unit-size * 4; + } +} + +.cvat-models-item-description { + font-size: 14px; + display: flex; + justify-content: space-between; + + > div > span:nth-child(2) { + margin-left: $grid-unit-size; + } + + button { + position: relative; + color: black; + margin-top: -$grid-unit-size * 2; + margin-right: -$grid-unit-size; + } + + &:hover { + cursor: pointer; + } +} + +.cvat-model-info-modal { + .ant-modal-body { + position: relative; + padding: 0; + + >.cvat-model-info-container:not(:last-child) { + padding: 0 $grid-unit-size * 3; + } + + >.cvat-model-info-container:last-child { + padding: 0 $grid-unit-size * 3 $grid-unit-size * 3 $grid-unit-size * 3; + } + } + + h3 { + margin-top: $grid-unit-size * 2; + } +} + +.cvat-model-info-modal-labels-title { + font-size: 16px; +} + +.cvat-model-info-modal-labels-list { + margin-top: $grid-unit-size; + max-height: $grid-unit-size * 18; + overflow: auto; + + .ant-tag { + margin-top: $grid-unit-size; + } +} + +.cvat-models-item-title { + &:hover { + cursor: pointer !important; + } +} + +.cvat-models-item-text-description { + max-width: 80%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + max-height: $grid-unit-size * 3; + display: inline; +} 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 00000000..cf2f08b0 --- /dev/null +++ b/cvat-ui/src/components/models-page/top-bar.tsx @@ -0,0 +1,89 @@ +// Copyright (C) 2022-2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useState } from 'react'; +import { Row, Col } from 'antd/lib/grid'; +import { PlusOutlined } from '@ant-design/icons'; +import Button from 'antd/lib/button'; +import { Input } from 'antd'; +import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering'; +import { ModelsQuery } from 'reducers'; +import { + localStorageRecentKeyword, localStorageRecentCapacity, config, +} from './models-filter-configuration'; + +const FilteringComponent = ResourceFilterHOC( + config, localStorageRecentKeyword, localStorageRecentCapacity, +); + +interface VisibleTopBarProps { + onApplyFilter(filter: string | null): void; + onApplySorting(sorting: string | null): void; + onApplySearch(search: string | null): void; + query: ModelsQuery; + onCreateModel(): void; + disabled?: boolean; +} + +export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element { + const { + query, onApplyFilter, onApplySorting, onApplySearch, onCreateModel, disabled, + } = props; + const [visibility, setVisibility] = useState(defaultVisibility); + + return ( + + +
+ { + onApplySearch(phrase); + }} + defaultValue={query.search || ''} + className='cvat-webhooks-page-search-bar' + placeholder='Search ...' + /> +
+ ( + setVisibility({ ...defaultVisibility, sorting: visible }) + )} + defaultFields={query.sort?.split(',') || ['-ID']} + sortingFields={['ID', 'Target URL', 'Owner', 'Description', 'Type', 'Updated date']} + onApplySorting={onApplySorting} + /> + ( + setVisibility({ ...defaultVisibility, predefined: visible }) + )} + onBuilderVisibleChange={(visible: boolean) => ( + setVisibility({ ...defaultVisibility, builder: visible }) + )} + onRecentVisibleChange={(visible: boolean) => ( + setVisibility({ + ...defaultVisibility, + builder: visibility.builder, + recent: visible, + }) + )} + onApplyFilter={onApplyFilter} + /> +
+
+
+
+ +
+ ); +} diff --git a/cvat-ui/src/components/resource-sorting-filtering/filtering.tsx b/cvat-ui/src/components/resource-sorting-filtering/filtering.tsx index de720f61..9c2fc27d 100644 --- a/cvat-ui/src/components/resource-sorting-filtering/filtering.tsx +++ b/cvat-ui/src/components/resource-sorting-filtering/filtering.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -26,6 +26,7 @@ interface ResourceFilterProps { recentVisible: boolean; builderVisible: boolean; value: string | null; + disabled?: boolean; onPredefinedVisibleChange?: (visible: boolean) => void; onBuilderVisibleChange(visible: boolean): void; onRecentVisibleChange(visible: boolean): void; @@ -117,6 +118,7 @@ export default function ResourceFilterHOC( const { predefinedVisible, builderVisible, recentVisible, value, onPredefinedVisibleChange, onBuilderVisibleChange, onRecentVisibleChange, onApplyFilter, + disabled, } = props; const user = useSelector((state: CombinedState) => state.auth.user); @@ -248,6 +250,7 @@ export default function ResourceFilterHOC( ) : null }