diff --git a/CHANGELOG.md b/CHANGELOG.md index abc9800b0614..18e5ca299eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + +## \[2.14.4\] - 2024-06-20 + +### Added + +- Polyline editing may be finished using corresponding shortcut + () + +### Changed + +- Single shape annotation mode allows to modify/delete objects + () + +### Fixed + +- Invalid server cache cleanup for backups and events (after #7864) + () + +- Filters by created date, updated date do not work on different pages (e.g. list of tasks or jobs) + () + ## \[2.14.3\] - 2024-06-13 diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts index 84e4bca31e4b..1bcc7ddb961a 100644 --- a/cvat-canvas/src/typescript/canvas.ts +++ b/cvat-canvas/src/typescript/canvas.ts @@ -11,6 +11,7 @@ import { CanvasModel, CanvasModelImpl, RectDrawingMethod, CuboidDrawingMethod, Configuration, Geometry, Mode, HighlightSeverity as _HighlightSeverity, CanvasHint as _CanvasHint, + PolyEditData, } from './canvasModel'; import { Master } from './master'; import { CanvasController, CanvasControllerImpl } from './canvasController'; @@ -35,7 +36,7 @@ interface Canvas { interact(interactionData: InteractionData): void; draw(drawData: DrawData): void; - edit(editData: MasksEditData): void; + edit(editData: MasksEditData | PolyEditData): void; group(groupData: GroupData): void; join(joinData: JoinData): void; slice(sliceData: SliceData): void; @@ -137,7 +138,7 @@ class CanvasImpl implements Canvas { this.model.draw(drawData); } - public edit(editData: MasksEditData): void { + public edit(editData: MasksEditData | PolyEditData): void { this.model.edit(editData); } diff --git a/cvat-canvas/src/typescript/canvasController.ts b/cvat-canvas/src/typescript/canvasController.ts index 9acf58e95fc2..82ec611be3e4 100644 --- a/cvat-canvas/src/typescript/canvasController.ts +++ b/cvat-canvas/src/typescript/canvasController.ts @@ -20,6 +20,7 @@ import { Configuration, MasksEditData, HighlightedElements, + PolyEditData, } from './canvasModel'; export interface CanvasController { @@ -30,7 +31,7 @@ export interface CanvasController { readonly activeElement: ActiveElement; readonly highlightedElements: HighlightedElements; readonly drawData: DrawData; - readonly editData: MasksEditData; + readonly editData: MasksEditData | PolyEditData; readonly interactionData: InteractionData; readonly mergeData: MergeData; readonly splitData: SplitData; @@ -44,7 +45,7 @@ export interface CanvasController { zoom(x: number, y: number, direction: number): void; draw(drawData: DrawData): void; - edit(editData: MasksEditData): void; + edit(editData: MasksEditData | PolyEditData): void; enableDrag(x: number, y: number): void; drag(x: number, y: number): void; disableDrag(): void; @@ -96,7 +97,7 @@ export class CanvasControllerImpl implements CanvasController { this.model.draw(drawData); } - public edit(editData: MasksEditData): void { + public edit(editData: MasksEditData | PolyEditData): void { this.model.edit(editData); } @@ -136,7 +137,7 @@ export class CanvasControllerImpl implements CanvasController { return this.model.drawData; } - public get editData(): MasksEditData { + public get editData(): MasksEditData | PolyEditData { return this.model.editData; } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 426a76acfd62..181111396267 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -147,8 +147,8 @@ export interface InteractionResult { export interface PolyEditData { enabled: boolean; - state: any; - pointID: number; + state?: any; + pointID?: number; } export interface MasksEditData { @@ -249,7 +249,7 @@ export interface CanvasModel { readonly activeElement: ActiveElement; readonly highlightedElements: HighlightedElements; readonly drawData: DrawData; - readonly editData: MasksEditData; + readonly editData: MasksEditData | PolyEditData; readonly interactionData: InteractionData; readonly mergeData: MergeData; readonly splitData: SplitData; @@ -275,7 +275,7 @@ export interface CanvasModel { grid(stepX: number, stepY: number): void; draw(drawData: DrawData): void; - edit(editData: MasksEditData): void; + edit(editData: MasksEditData | PolyEditData): void; group(groupData: GroupData): void; join(joinData: JoinData): void; slice(sliceData: SliceData): void; @@ -369,7 +369,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { fittedScale: number; zLayer: number | null; drawData: DrawData; - editData: MasksEditData; + editData: MasksEditData | PolyEditData; interactionData: InteractionData; mergeData: MergeData; groupData: GroupData; @@ -780,7 +780,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.notify(UpdateReasons.DRAW); } - public edit(editData: MasksEditData): void { + public edit(editData: MasksEditData | PolyEditData): void { if (![Mode.IDLE, Mode.EDIT].includes(this.data.mode)) { throw Error(`Canvas is busy. Action: ${this.data.mode}`); } @@ -1083,7 +1083,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { return { ...this.data.drawData }; } - public get editData(): MasksEditData { + public get editData(): MasksEditData | PolyEditData { return { ...this.data.editData }; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 4b348903aae3..398a6262c5d3 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1642,6 +1642,8 @@ export class CanvasViewImpl implements CanvasView, Listener { this.masksHandler.edit(data); } else if (this.masksHandler.enabled) { this.masksHandler.edit(data); + } else if (this.editHandler.enabled && this.editHandler.shapeType === 'polyline') { + this.editHandler.edit(data); } } else if (reason === UpdateReasons.INTERACT) { const data: InteractionData = this.controller.interactionData; @@ -1854,18 +1856,18 @@ export class CanvasViewImpl implements CanvasView, Listener { const { points } = state; const [left, top, right, bottom] = points.slice(-4); const imageBitmap = expandChannels(255, 255, 255, points); - imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, - (dataURL: string) => new Promise((resolve) => { - if (bitmapUpdateReqId === this.bitmapUpdateReqId) { - const img = document.createElement('img'); - img.addEventListener('load', () => { - ctx.drawImage(img, left, top); - URL.revokeObjectURL(dataURL); - resolve(); - }); - img.src = dataURL; - } - })); + imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, (dataURL: string) => new + Promise((resolve) => { + if (bitmapUpdateReqId === this.bitmapUpdateReqId) { + const img = document.createElement('img'); + img.addEventListener('load', () => { + ctx.drawImage(img, left, top); + URL.revokeObjectURL(dataURL); + resolve(); + }); + img.src = dataURL; + } + })); } if (state.shapeType === 'cuboid') { diff --git a/cvat-canvas/src/typescript/editHandler.ts b/cvat-canvas/src/typescript/editHandler.ts index 89e61881a57c..567eea29c7de 100644 --- a/cvat-canvas/src/typescript/editHandler.ts +++ b/cvat-canvas/src/typescript/editHandler.ts @@ -16,6 +16,8 @@ export interface EditHandler { transform(geometry: Geometry): void; configurate(configuration: Configuration): void; cancel(): void; + enabled: boolean; + shapeType: string; } export class EditHandlerImpl implements EditHandler { @@ -31,9 +33,9 @@ export class EditHandlerImpl implements EditHandler { private autobordersEnabled: boolean; private intelligentCutEnabled: boolean; private outlinedBorders: string; + private isEditing: boolean; private setupTrailingPoint(circle: SVG.Circle): void { - const head = this.editedShape.attr('points').split(' ').slice(0, this.editData.pointID).join(' '); circle.on('mouseenter', (): void => { circle.attr({ 'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale, @@ -46,22 +48,9 @@ export class EditHandlerImpl implements EditHandler { }); }); - const minimumPoints = 2; circle.on('mousedown', (e: MouseEvent): void => { if (e.button !== 0) return; - const { offset } = this.geometry; - const stringifiedPoints = `${head} ${this.editLine.node.getAttribute('points').slice(0, -2)}`; - const points = pointsToNumberArray(stringifiedPoints) - .slice(0, -2) - .map((coord: number): number => coord - offset); - - if (points.length >= minimumPoints * 2) { - const { state } = this.editData; - this.edit({ - enabled: false, - }); - this.onEditDone(state, points); - } + this.edit({ enabled: false }); }); } @@ -345,6 +334,7 @@ export class EditHandlerImpl implements EditHandler { this.canvas.off('mousedown.edit'); this.canvas.off('mousemove.edit'); this.autoborderHandler.autoborder(false); + this.isEditing = false; if (this.editedShape) { this.setupPoints(false); @@ -372,6 +362,7 @@ export class EditHandlerImpl implements EditHandler { .clone().attr('stroke', this.outlinedBorders); this.setupPoints(true); this.startEdit(); + this.isEditing = true; // draw points for this with selected and start editing till another point is clicked // click one of two parts to remove (in case of polygon only) @@ -380,6 +371,18 @@ export class EditHandlerImpl implements EditHandler { } private closeEditing(): void { + if (this.isEditing && this.editData.state.shapeType === 'polyline') { + const { offset } = this.geometry; + const head = this.editedShape.attr('points').split(' ').slice(0, this.editData.pointID).join(' '); + const stringifiedPoints = `${head} ${this.editLine.node.getAttribute('points').slice(0, -2)}`; + const points = pointsToNumberArray(stringifiedPoints) + .slice(0, -2) + .map((coord: number): number => coord - offset); + if (points.length >= 2 * 2) { // minimumPoints * 2 + const { state } = this.editData; + this.onEditDone(state, points); + } + } this.release(); } @@ -400,11 +403,12 @@ export class EditHandlerImpl implements EditHandler { this.editLine = null; this.geometry = null; this.clones = []; + this.isEditing = false; } public edit(editData: any): void { if (editData.enabled) { - if (editData.state.shapeType !== 'rectangle') { + if (['polygon', 'polyline', 'points'].includes(editData.state.shapeType)) { this.editData = editData; this.initEditing(); } else { @@ -421,6 +425,14 @@ export class EditHandlerImpl implements EditHandler { this.onEditDone(null, null); } + get enabled(): boolean { + return this.isEditing; + } + + get shapeType(): string { + return this.editData.state.shapeType; + } + public configurate(configuration: Configuration): void { this.autobordersEnabled = configuration.autoborders; this.outlinedBorders = configuration.outlinedBorders || 'black'; diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt index 05201fb33f02..0d48f98de939 100644 --- a/cvat-cli/requirements/base.txt +++ b/cvat-cli/requirements/base.txt @@ -1,3 +1,3 @@ -cvat-sdk~=2.14.3 +cvat-sdk~=2.14.4 Pillow>=10.3.0 setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py index fc687842ab48..7497d5e3c100 100644 --- a/cvat-cli/src/cvat_cli/version.py +++ b/cvat-cli/src/cvat_cli/version.py @@ -1 +1 @@ -VERSION = "2.14.3" +VERSION = "2.14.4" diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index 257e18fbad24..d1a8c8d24bfb 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -24,12 +24,6 @@ import { import AnnotationHistory from './annotations-history'; import { Job } from './session'; -interface ImportedCollection { - tags: Tag[], - shapes: Shape[], - tracks: Track[], -} - const validateAttributesList = ( attributes: { spec_id: number, value: string }[], ): { spec_id: number, value: string }[] => { @@ -116,7 +110,11 @@ export default class Collection { }; } - public import(data: Omit): ImportedCollection { + public import(data: Omit): { + tags: Tag[]; + shapes: Shape[]; + tracks: Track[]; + } { const result = { tags: [], shapes: [], @@ -181,7 +179,7 @@ export default class Collection { return data; } - public get(frame: number, allTracks: boolean, filters: string[]): ObjectState[] { + public get(frame: number, allTracks: boolean, filters: object[]): ObjectState[] { const { tracks } = this; const shapes = this.shapes[frame] || []; const tags = this.tags[frame] || []; @@ -774,47 +772,56 @@ export default class Collection { ); } - public clear(startframe: number, endframe: number, delTrackKeyframesOnly: boolean): void { - if (startframe !== undefined && endframe !== undefined) { + public clear(options?: { + startFrame?: number; + stopFrame?: number; + delTrackKeyframesOnly?: boolean; + }): void { + const { startFrame, stopFrame, delTrackKeyframesOnly } = options ?? {}; + + if (typeof startFrame === 'undefined' && typeof stopFrame === 'undefined') { + this.shapes = {}; + this.tags = {}; + this.tracks = []; + this.objects = {}; + + this.flush = true; + } else { + const from = startFrame ?? 0; + const to = stopFrame ?? this.stopFrame; + // If only a range of annotations need to be cleared - for (let frame = startframe; frame <= endframe; frame++) { + for (let frame = from; frame <= to; frame++) { this.shapes[frame] = []; this.tags[frame] = []; } - const { tracks } = this; - tracks.forEach((track) => { - if (track.frame <= endframe) { + + this.tracks.slice(0).forEach((track) => { + if (track.frame <= to) { if (delTrackKeyframesOnly) { for (const keyframe of Object.keys(track.shapes)) { - if (+keyframe >= startframe && +keyframe <= endframe) { + if (+keyframe >= from && +keyframe <= to) { delete track.shapes[keyframe]; - ((track as unknown as SkeletonTrack).elements || []).forEach((element) => { - if (keyframe in element.shapes) { - delete element.shapes[keyframe]; - element.updated = Date.now(); - } - }); + if (track instanceof SkeletonTrack) { + track.elements.forEach((element) => { + if (keyframe in element.shapes) { + delete element.shapes[keyframe]; + element.updated = Date.now(); + } + }); + } track.updated = Date.now(); } } - } else if (track.frame >= startframe) { - const index = tracks.indexOf(track); - if (index > -1) { tracks.splice(index, 1); } + + if (Object.keys(track.shapes).length === 0) { + this.tracks.splice(this.tracks.indexOf(track), 1); + } + } else if (track.frame >= from) { + this.tracks.splice(this.tracks.indexOf(track), 1); } } }); - } else if (startframe === undefined && endframe === undefined) { - // If all annotations need to be cleared - this.shapes = {}; - this.tags = {}; - this.tracks = []; - this.objects = {}; - - this.flush = true; - } else { - // If inputs provided were wrong - throw Error('Could not remove the annotations, please provide both inputs or' + - ' leave the inputs below empty to remove all the annotations from this job'); } } diff --git a/cvat-core/src/annotations.ts b/cvat-core/src/annotations.ts index 8e3b74b4a182..3c63f800426a 100644 --- a/cvat-core/src/annotations.ts +++ b/cvat-core/src/annotations.ts @@ -42,7 +42,7 @@ function getSession(session): WeakMapItem { } throw new InstanceNotInitializedError( - 'Session has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + 'Session has not been initialized yet. Call annotations.get() or annotations.clear({ reload: true }) before', ); } @@ -113,17 +113,24 @@ export async function getAnnotations(session, frame, allTracks, filters): Promis } } -export async function clearAnnotations(session, reload, startframe, endframe, delTrackKeyframesOnly): Promise { - checkObjectType('reload', reload, 'boolean', null); +export async function clearAnnotations( + session: Task | Job, + options: Parameters[0], +): Promise { const sessionType = session instanceof Task ? 'task' : 'job'; const cache = getCache(sessionType); - if (reload) { - cache.delete(session); - return getAnnotationsFromServer(session); + if (Object.hasOwn(options ?? {}, 'reload')) { + const { reload } = options; + checkObjectType('reload', reload, 'boolean', null); + + if (reload) { + cache.delete(session); + return getAnnotationsFromServer(session); + } } - return getCollection(session).clear(startframe, endframe, delTrackKeyframesOnly); + return getCollection(session).clear(options); } export async function exportDataset( diff --git a/cvat-core/src/core-types.ts b/cvat-core/src/core-types.ts index 0edb2dce84cf..e72e32041f00 100644 --- a/cvat-core/src/core-types.ts +++ b/cvat-core/src/core-types.ts @@ -38,7 +38,7 @@ export interface SerializedModel { labels_v2?: MLModelLabel[]; version?: number; description?: string; - kind?: ModelKind; + kind?: ModelKind | 'classifier'; type?: string; return_type?: ModelReturnType; owner?: any; diff --git a/cvat-core/src/enums.ts b/cvat-core/src/enums.ts index 226ed8e3df87..5543d4712af4 100644 --- a/cvat-core/src/enums.ts +++ b/cvat-core/src/enums.ts @@ -159,7 +159,6 @@ export enum ModelKind { DETECTOR = 'detector', INTERACTOR = 'interactor', TRACKER = 'tracker', - CLASSIFIER = 'classifier', REID = 'reid', } diff --git a/cvat-core/src/ml-model.ts b/cvat-core/src/ml-model.ts index a504986c038d..e632328d6250 100644 --- a/cvat-core/src/ml-model.ts +++ b/cvat-core/src/ml-model.ts @@ -40,9 +40,19 @@ export default class MLModel { } public get kind(): ModelKind { + // compatibility alias; TODO: remove this + if (this.serialized.kind === 'classifier') return ModelKind.DETECTOR; return this.serialized.kind; } + public get displayKind(): string { + if (this.kind === ModelKind.DETECTOR) { + if (this.returnType === ModelReturnType.TAG) return 'classifier'; + if (this.returnType === ModelReturnType.MASK) return 'segmenter'; + } + return this.kind; + } + public get params(): ModelParams { const result: ModelParams = { canvas: { diff --git a/cvat-core/src/project-implementation.ts b/cvat-core/src/project-implementation.ts index 8a274d4ee308..028f535cab61 100644 --- a/cvat-core/src/project-implementation.ts +++ b/cvat-core/src/project-implementation.ts @@ -1,140 +1,172 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import { Storage } from './storage'; import serverProxy from './server-proxy'; import { decodePreview } from './frames'; -import Project from './project'; +import ProjectClass from './project'; import { exportDataset, importDataset } from './annotations'; import { SerializedLabel } from './server-response-types'; import { Label } from './labels'; import AnnotationGuide from './guide'; -export default function implementProject(projectClass) { - projectClass.prototype.save.implementation = async function () { - if (typeof this.id !== 'undefined') { - const projectData = this._updateTrigger.getUpdated(this, { - bugTracker: 'bug_tracker', - assignee: 'assignee_id', - }); +export default function implementProject(Project: typeof ProjectClass): typeof ProjectClass { + Object.defineProperty(Project.prototype.save, 'implementation', { + value: async function saveImplementation( + this: ProjectClass, + ): ReturnType { + if (typeof this.id !== 'undefined') { + const projectData = this._updateTrigger.getUpdated(this, { + bugTracker: 'bug_tracker', + assignee: 'assignee_id', + }); + + if (projectData.assignee_id) { + projectData.assignee_id = projectData.assignee_id.id; + } - if (projectData.assignee_id) { - projectData.assignee_id = projectData.assignee_id.id; - } + await Promise.all((projectData.labels || []).map((label: Label): Promise => { + if (label.deleted) { + return serverProxy.labels.delete(label.id); + } - await Promise.all((projectData.labels || []).map((label: Label): Promise => { - if (label.deleted) { - return serverProxy.labels.delete(label.id); + if (label.patched) { + return serverProxy.labels.update(label.id, label.toJSON()); + } + + return Promise.resolve(); + })); + + // leave only new labels to create them via project PATCH request + projectData.labels = (projectData.labels || []) + .filter((label: SerializedLabel) => !Number.isInteger(label.id)).map((el) => el.toJSON()); + if (!projectData.labels.length) { + delete projectData.labels; } - if (label.patched) { - return serverProxy.labels.update(label.id, label.toJSON()); + this._updateTrigger.reset(); + let serializedProject = null; + if (Object.keys(projectData).length) { + serializedProject = await serverProxy.projects.save(this.id, projectData); + } else { + [serializedProject] = (await serverProxy.projects.get({ id: this.id })); } - return Promise.resolve(); - })); + const labels = await serverProxy.labels.get({ project_id: serializedProject.id }); + return new Project({ ...serializedProject, labels: labels.results }); + } + + // initial creating + const projectSpec: any = { + name: this.name, + labels: this.labels.map((el) => el.toJSON()), + }; + + if (this.bugTracker) { + projectSpec.bug_tracker = this.bugTracker; + } + + if (this.targetStorage) { + projectSpec.target_storage = this.targetStorage.toJSON(); + } - // leave only new labels to create them via project PATCH request - projectData.labels = (projectData.labels || []) - .filter((label: SerializedLabel) => !Number.isInteger(label.id)).map((el) => el.toJSON()); - if (!projectData.labels.length) { - delete projectData.labels; + if (this.sourceStorage) { + projectSpec.source_storage = this.sourceStorage.toJSON(); } - this._updateTrigger.reset(); - let serializedProject = null; - if (Object.keys(projectData).length) { - serializedProject = await serverProxy.projects.save(this.id, projectData); - } else { - [serializedProject] = (await serverProxy.projects.get({ id: this.id })); + const project = await serverProxy.projects.create(projectSpec); + const labels = await serverProxy.labels.get({ project_id: project.id }); + return new Project({ ...project, labels: labels.results }); + }, + }); + + Object.defineProperty(Project.prototype.delete, 'implementation', { + value: function deleteImplementation( + this: ProjectClass, + ): ReturnType { + return serverProxy.projects.delete(this.id); + }, + }); + + Object.defineProperty(Project.prototype.preview, 'implementation', { + value: function previewImplementation( + this: ProjectClass, + ): ReturnType { + if (this.id === null) { + return Promise.resolve(''); } + return serverProxy.projects.getPreview(this.id).then((preview) => { + if (!preview) { + return Promise.resolve(''); + } + return decodePreview(preview); + }); + }, + }); + + Object.defineProperty(Project.prototype.annotations.exportDataset, 'implementation', { + value: function exportDatasetImplementation( + this: ProjectClass, + format: Parameters[0], + saveImages: Parameters[1], + useDefaultSettings: Parameters[2], + targetStorage: Parameters[3], + customName: Parameters[4], + ): ReturnType { + return exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); + }, + }); + + Object.defineProperty(Project.prototype.annotations.importDataset, 'implementation', { + value: function importDatasetImplementation( + this: ProjectClass, + format: Parameters[0], + useDefaultSettings: Parameters[1], + sourceStorage: Parameters[2], + file: Parameters[3], + options: Parameters[4], + ): ReturnType { + return importDataset(this, format, useDefaultSettings, sourceStorage, file, options); + }, + }); + + Object.defineProperty(Project.prototype.backup, 'implementation', { + value: function backupImplementation( + this: ProjectClass, + targetStorage: Parameters[0], + useDefaultSettings: Parameters[1], + fileName: Parameters[2], + ): ReturnType { + return serverProxy.projects.backup(this.id, targetStorage, useDefaultSettings, fileName); + }, + }); + + Object.defineProperty(Project.restore, 'implementation', { + value: async function restoreImplementation( + this: ProjectClass, + storage: Parameters[0], + file: Parameters[1], + ): ReturnType { + const serializedProject = await serverProxy.projects.restore(storage, file); const labels = await serverProxy.labels.get({ project_id: serializedProject.id }); return new Project({ ...serializedProject, labels: labels.results }); - } - - // initial creating - const projectSpec: any = { - name: this.name, - labels: this.labels.map((el) => el.toJSON()), - }; - - if (this.bugTracker) { - projectSpec.bug_tracker = this.bugTracker; - } - - if (this.targetStorage) { - projectSpec.target_storage = this.targetStorage.toJSON(); - } - - if (this.sourceStorage) { - projectSpec.source_storage = this.sourceStorage.toJSON(); - } - - const project = await serverProxy.projects.create(projectSpec); - const labels = await serverProxy.labels.get({ project_id: project.id }); - return new Project({ ...project, labels: labels.results }); - }; - - projectClass.prototype.delete.implementation = async function () { - const result = await serverProxy.projects.delete(this.id); - return result; - }; - - projectClass.prototype.preview.implementation = async function (this: Project): Promise { - if (this.id === null) return ''; - const preview = await serverProxy.projects.getPreview(this.id); - if (!preview) return ''; - return decodePreview(preview); - }; - - projectClass.prototype.annotations.exportDataset.implementation = async function ( - format: string, - saveImages: boolean, - useDefaultSettings: boolean, - targetStorage: Storage, - customName?: string, - ) { - const result = exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); - return result; - }; - projectClass.prototype.annotations.importDataset.implementation = async function ( - format: string, - useDefaultSettings: boolean, - sourceStorage: Storage, - file: File | string, - options?: { - convMaskToPoly?: boolean, - updateStatusCallback?: (s: string, n: number) => void, }, - ) { - return importDataset(this, format, useDefaultSettings, sourceStorage, file, options); - }; - - projectClass.prototype.backup.implementation = async function ( - targetStorage: Storage, - useDefaultSettings: boolean, - fileName?: string, - ) { - const result = await serverProxy.projects.backup(this.id, targetStorage, useDefaultSettings, fileName); - return result; - }; - - projectClass.restore.implementation = async function (storage: Storage, file: File | string) { - const result = await serverProxy.projects.restore(storage, file); - return result; - }; - - projectClass.prototype.guide.implementation = async function guide() { - if (this.guideId === null) { - return null; - } - - const result = await serverProxy.guides.get(this.guideId); - return new AnnotationGuide(result); - }; - - return projectClass; + }); + + Object.defineProperty(Project.prototype.guide, 'implementation', { + value: async function guideImplementation( + this: ProjectClass, + ): ReturnType { + if (this.guideId === null) { + return null; + } + + const result = await serverProxy.guides.get(this.guideId); + return new AnnotationGuide(result); + }, + }); + + return Project; } diff --git a/cvat-core/src/project.ts b/cvat-core/src/project.ts index fd147472837d..d96f7361b610 100644 --- a/cvat-core/src/project.ts +++ b/cvat-core/src/project.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -31,8 +31,23 @@ export default class Project { public readonly targetStorage: Storage; public labels: Label[]; public annotations: { - exportDataset: CallableFunction; - importDataset: CallableFunction; + exportDataset: ( + format: string, + saveImages: boolean, + useDefaultSettings: boolean, + targetStorage: Storage, + name?: string, + ) => Promise; + importDataset: ( + format: string, + useDefaultSettings: boolean, + sourceStorage: Storage, + file: File | string, + options?: { + convMaskToPoly?: boolean, + updateStatusCallback?: (s: string, n: number) => void, + }, + ) => Promise; }; constructor(initialData: Readonly) { @@ -198,30 +213,29 @@ export default class Project { ); // When we call a function, for example: project.annotations.get() - // In the method get we lose the project context - // So, we need to bind it + // In the method get we lose the project context, so, we need to bind it this.annotations = { exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), importDataset: Object.getPrototypeOf(this).annotations.importDataset.bind(this), }; } - async preview() { + async preview(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.preview); return result; } - async save() { + async save(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save); return result; } - async delete() { + async delete(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete); return result; } - async backup(targetStorage: Storage, useDefaultSettings: boolean, fileName?: string) { + async backup(targetStorage: Storage, useDefaultSettings: boolean, fileName?: string): Promise { const result = await PluginRegistry.apiWrapper.call( this, Project.prototype.backup, @@ -232,7 +246,7 @@ export default class Project { return result; } - static async restore(storage: Storage, file: File | string) { + static async restore(storage: Storage, file: File | string): Promise { const result = await PluginRegistry.apiWrapper.call(this, Project.restore, storage, file); return result; } @@ -249,11 +263,11 @@ Object.defineProperties( annotations: Object.freeze({ value: { async exportDataset( - format: string, - saveImages: boolean, - useDefaultSettings: boolean, - targetStorage: Storage, - customName?: string, + format: Parameters[0], + saveImages: Parameters[1], + useDefaultSettings: Parameters[2], + targetStorage: Parameters[3], + customName: Parameters[4], ) { const result = await PluginRegistry.apiWrapper.call( this, @@ -267,14 +281,11 @@ Object.defineProperties( return result; }, async importDataset( - format: string, - useDefaultSettings: boolean, - sourceStorage: Storage, - file: File | string, - options?: { - convMaskToPoly?: boolean, - updateStatusCallback?: (s: string, n: number) => void, - }, + format: Parameters[0], + useDefaultSettings: Parameters[1], + sourceStorage: Parameters[2], + file: Parameters[3], + options: Parameters[4], ) { const result = await PluginRegistry.apiWrapper.call( this, diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 7a38b48dd401..c59354b4d242 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -1004,7 +1004,7 @@ async function backupTask(id: number, targetStorage: Storage, useDefaultSettings }); } -async function restoreTask(storage: Storage, file: File | string) { +async function restoreTask(storage: Storage, file: File | string): Promise { const { backendAPI } = config; // keep current default params to 'freeze" them during this request const params: Params = { @@ -1110,7 +1110,7 @@ async function backupProject( }); } -async function restoreProject(storage: Storage, file: File | string) { +async function restoreProject(storage: Storage, file: File | string): Promise { const { backendAPI } = config; // keep current default params to 'freeze" them during this request const params: Params = { diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 124424f78fb8..b26a2675804f 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -5,8 +5,7 @@ import { omit } from 'lodash'; import { ArgumentError } from './exceptions'; -import { HistoryActions, JobType, RQStatus } from './enums'; -import { Storage } from './storage'; +import { HistoryActions, JobType } from './enums'; import { Task as TaskClass, Job as JobClass } from './session'; import logger from './logger'; import serverProxy from './server-proxy'; @@ -54,781 +53,1165 @@ async function restoreFrameWrapper(jobID, frame): Promise { }, redo, [], frame); } -export function implementJob(Job) { - Job.prototype.save.implementation = async function (additionalData: any) { - if (this.id) { - const jobData = this._updateTrigger.getUpdated(this); - if (jobData.assignee) { - jobData.assignee = jobData.assignee.id; - } - - let updatedJob = null; - try { - const data = await serverProxy.jobs.save(this.id, jobData); - updatedJob = new Job(data); - } catch (error) { - updatedJob = new Job(this._initialData); - throw error; - } finally { - this.stage = updatedJob.stage; - this.state = updatedJob.state; - this.assignee = updatedJob.assignee; - this._updateTrigger.reset(); - } - - return this; - } - - const jobSpec = { - ...(this.assignee ? { assignee: this.assignee.id } : {}), - ...(this.stage ? { stage: this.stage } : {}), - ...(this.state ? { stage: this.state } : {}), - type: this.type, - task_id: this.taskId, - }; - const job = await serverProxy.jobs.create({ ...jobSpec, ...additionalData }); - return new Job(job); - }; - - Job.prototype.delete.implementation = async function () { - if (this.type !== JobType.GROUND_TRUTH) { - throw new Error('Only ground truth job can be deleted'); - } - const result = await serverProxy.jobs.delete(this.id); - return result; - }; - - Job.prototype.issues.implementation = async function () { - const result = await serverProxy.issues.get({ job_id: this.id }); - return result.map((issue) => new Issue(issue)); - }; - - Job.prototype.openIssue.implementation = async function (issue, message) { - checkObjectType('issue', issue, null, Issue); - checkObjectType('message', message, 'string'); - const result = await serverProxy.issues.create({ - ...issue.serialize(), - message, - }); - return new Issue(result); - }; - - Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } - - if (frame < this.startFrame || frame > this.stopFrame) { - throw new ArgumentError(`The frame with number ${frame} is out of the job`); - } - - const frameData = await getFrame( - this.id, - this.dataChunkSize, - this.dataChunkType, - this.mode, - frame, - this.startFrame, - this.stopFrame, - isPlaying, - step, - this.dimension, - (chunkNumber, quality) => this.frames.chunk(chunkNumber, quality), - ); - return frameData; - }; - - Job.prototype.frames.delete.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } - - if (frame < this.startFrame || frame > this.stopFrame) { - throw new Error('The frame is out of the job'); - } - - await deleteFrameWrapper.call(this, this.id, frame); - }; - - Job.prototype.frames.restore.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } - - if (frame < this.startFrame || frame > this.stopFrame) { - throw new Error('The frame is out of the job'); - } - - await restoreFrameWrapper.call(this, this.id, frame); - }; - - Job.prototype.frames.save.implementation = async function () { - const result = await patchMeta(this.id); - return result; - }; - - Job.prototype.frames.cachedChunks.implementation = async function () { - const cachedChunks = await getCachedChunks(this.id); - return cachedChunks; - }; - - Job.prototype.frames.preview.implementation = async function (this: JobClass): Promise { - if (this.id === null || this.taskId === null) return ''; - const preview = await serverProxy.jobs.getPreview(this.id); - if (!preview) return ''; - return decodePreview(preview); - }; - - Job.prototype.frames.contextImage.implementation = async function (frameId) { - const result = await getContextImage(this.id, frameId); - return result; - }; - - Job.prototype.frames.chunk.implementation = async function (chunkNumber, quality) { - const result = await serverProxy.frames.getData(this.id, chunkNumber, quality); - return result; - }; - - Job.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { - if (typeof filters !== 'object') { - throw new ArgumentError('Filters should be an object'); - } - - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } - - if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { - throw new ArgumentError('The start frame is out of the job'); - } - - if (frameTo < this.startFrame || frameTo > this.stopFrame) { - throw new ArgumentError('The stop frame is out of the job'); - } - - return findFrame(this.id, frameFrom, frameTo, filters); - }; - - Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { - if (!Array.isArray(filters)) { - throw new ArgumentError('Filters must be an array'); - } - - if (!Number.isInteger(frame)) { - throw new ArgumentError('The frame argument must be an integer'); - } - - if (frame < this.startFrame || frame > this.stopFrame) { - throw new ArgumentError(`Frame ${frame} does not exist in the job`); - } +export function implementJob(Job: typeof JobClass): typeof JobClass { + Object.defineProperty(Job.prototype.save, 'implementation', { + value: async function saveImplementation( + this: JobClass, + additionalData: any, + ): ReturnType { + if (this.id) { + const jobData = this._updateTrigger.getUpdated(this); + if (jobData.assignee) { + jobData.assignee = jobData.assignee.id; + } - const annotationsData = await getAnnotations(this, frame, allTracks, filters); - const deletedFrames = await getDeletedFrames('job', this.id); - if (frame in deletedFrames) { - return []; - } + let updatedJob = null; + try { + const data = await serverProxy.jobs.save(this.id, jobData); + updatedJob = new Job(data); + } catch (error) { + updatedJob = new Job(this._initialData); + throw error; + } finally { + this.stage = updatedJob.stage; + this.state = updatedJob.state; + this.assignee = updatedJob.assignee; + this._updateTrigger.reset(); + } - return annotationsData; - }; + return this; + } - Job.prototype.annotations.search.implementation = function (frameFrom, frameTo, searchParameters) { - if ('annotationsFilters' in searchParameters && !Array.isArray(searchParameters.annotationsFilters)) { - throw new ArgumentError('Annotations filters must be an array'); - } + const jobSpec = { + ...(this.assignee ? { assignee: this.assignee.id } : {}), + ...(this.stage ? { stage: this.stage } : {}), + ...(this.state ? { stage: this.state } : {}), + type: this.type, + task_id: this.taskId, + }; + const job = await serverProxy.jobs.create({ ...jobSpec, ...additionalData }); + return new Job(job); + }, + }); + + Object.defineProperty(Job.prototype.delete, 'implementation', { + value: async function deleteImplementation( + this: JobClass, + ): ReturnType { + if (this.type !== JobType.GROUND_TRUTH) { + throw new Error('Only ground truth job can be deleted'); + } - if ('generalFilters' in searchParameters && typeof searchParameters.generalFilters.isEmptyFrame !== 'boolean') { - throw new ArgumentError('General filter isEmptyFrame must be a boolean'); - } + return serverProxy.jobs.delete(this.id); + }, + }); + + Object.defineProperty(Job.prototype.issues, 'implementation', { + value: function issuesImplementation( + this: JobClass, + ): ReturnType { + return serverProxy.issues.get({ job_id: this.id }) + .then((issues) => issues.map((issue) => new Issue(issue))); + }, + }); + + Object.defineProperty(Job.prototype.openIssue, 'implementation', { + value: async function openIssueImplementation( + this: JobClass, + issue: Parameters[0], + message: Parameters[1], + ): ReturnType { + checkObjectType('issue', issue, null, Issue); + checkObjectType('message', message, 'string'); + const result = await serverProxy.issues.create({ + ...issue.serialize(), + message, + }); + return new Issue(result); + }, + }); + + Object.defineProperty(Job.prototype.close, 'implementation', { + value: function closeImplementation( + this: JobClass, + ) { + clearFrames(this.id); + clearCache(this); + }, + }); + + Object.defineProperty(Job.prototype.guide, 'implementation', { + value: async function guideImplementation( + this: JobClass, + ): ReturnType { + if (this.guideId === null) { + return null; + } - if ('annotationsFilters' in searchParameters && 'generalFilters' in searchParameters) { - throw new ArgumentError('Both annotations filters and general fiters could not be used together'); - } + const result = await serverProxy.guides.get(this.guideId); + return new AnnotationGuide(result); + }, + }); + + Object.defineProperty(Job.prototype.frames.get, 'implementation', { + value: function getFrameImplementation( + this: JobClass, + frame: Parameters[0], + isPlaying: Parameters[1], + step: Parameters[2], + ): ReturnType { + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + } - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + if (frame < this.startFrame || frame > this.stopFrame) { + throw new ArgumentError(`The frame with number ${frame} is out of the job`); + } - if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { - throw new ArgumentError('The start frame is out of the job'); - } + return getFrame( + this.id, + this.dataChunkSize, + this.dataChunkType, + this.mode, + frame, + this.startFrame, + this.stopFrame, + isPlaying, + step, + this.dimension, + (chunkNumber, quality) => this.frames.chunk(chunkNumber, quality), + ); + }, + }); + + Object.defineProperty(Job.prototype.frames.delete, 'implementation', { + value: function deleteFrameImplementation( + this: JobClass, + frame: Parameters[0], + ): ReturnType { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } - if (frameTo < this.startFrame || frameTo > this.stopFrame) { - throw new ArgumentError('The stop frame is out of the job'); - } + if (frame < this.startFrame || frame > this.stopFrame) { + throw new Error('The frame is out of the job'); + } - return getCollection(this).search(frameFrom, frameTo, searchParameters); - }; + return deleteFrameWrapper.call(this, this.id, frame); + }, + }); + + Object.defineProperty(Job.prototype.frames.restore, 'implementation', { + value: function restoreFrameImplementation( + this: JobClass, + frame: Parameters[0], + ): ReturnType { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } - Job.prototype.annotations.save.implementation = async function (onUpdate) { - return getSaver(this).save(onUpdate); - }; + if (frame < this.startFrame || frame > this.stopFrame) { + throw new Error('The frame is out of the job'); + } - Job.prototype.annotations.merge.implementation = async function (objectStates) { - return getCollection(this).merge(objectStates); - }; + return restoreFrameWrapper.call(this, this.id, frame); + }, + }); + + Object.defineProperty(Job.prototype.frames.save, 'implementation', { + value: function saveFramesImplementation( + this: JobClass, + ): ReturnType { + return patchMeta(this.id); + }, + }); + + Object.defineProperty(Job.prototype.frames.cachedChunks, 'implementation', { + value: function cachedChunksImplementation( + this: JobClass, + ): ReturnType { + return Promise.resolve(getCachedChunks(this.id)); + }, + }); + + Object.defineProperty(Job.prototype.frames.preview, 'implementation', { + value: function previewImplementation( + this: JobClass, + ): ReturnType { + if (this.id === null || this.taskId === null) { + return Promise.resolve(''); + } - Job.prototype.annotations.split.implementation = async function (objectState, frame) { - return getCollection(this).split(objectState, frame); - }; + return serverProxy.jobs.getPreview(this.id).then((preview) => { + if (!preview) { + return Promise.resolve(''); + } + return decodePreview(preview); + }); + }, + }); + + Object.defineProperty(Job.prototype.frames.contextImage, 'implementation', { + value: function contextImageImplementation( + this: JobClass, + frameId: Parameters[0], + ): ReturnType { + return getContextImage(this.id, frameId); + }, + }); + + Object.defineProperty(Job.prototype.frames.chunk, 'implementation', { + value: function chunkImplementation( + this: JobClass, + chunkNumber: Parameters[0], + quality: Parameters[1], + ): ReturnType { + return serverProxy.frames.getData(this.id, chunkNumber, quality); + }, + }); + + Object.defineProperty(Job.prototype.frames.search, 'implementation', { + value: function searchFrameImplementation( + this: JobClass, + filters: Parameters[0], + frameFrom: Parameters[1], + frameTo: Parameters[2], + ): ReturnType { + if (typeof filters !== 'object') { + throw new ArgumentError('Filters should be an object'); + } - Job.prototype.annotations.group.implementation = async function (objectStates, reset) { - return getCollection(this).group(objectStates, reset); - }; + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - Job.prototype.annotations.join.implementation = async function (objectStates, points) { - return getCollection(this).join(objectStates, points); - }; + if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { + throw new ArgumentError('The start frame is out of the job'); + } - Job.prototype.annotations.slice.implementation = async function (objectState, results) { - return getCollection(this).slice(objectState, results); - }; + if (frameTo < this.startFrame || frameTo > this.stopFrame) { + throw new ArgumentError('The stop frame is out of the job'); + } - Job.prototype.annotations.hasUnsavedChanges.implementation = function () { - return getSaver(this).hasUnsavedChanges(); - }; + return findFrame(this.id, frameFrom, frameTo, filters); + }, + }); + + Object.defineProperty(Job.prototype.annotations.get, 'implementation', { + value: async function getAnnotationsImplementation( + this: JobClass, + frame: Parameters[0], + allTracks: Parameters[1], + filters: Parameters[2], + ): ReturnType { + if (!Array.isArray(filters)) { + throw new ArgumentError('Filters must be an array'); + } - Job.prototype.annotations.clear.implementation = async function ( - reload, startframe, endframe, delTrackKeyframesOnly, - ) { - const result = await clearAnnotations(this, reload, startframe, endframe, delTrackKeyframesOnly); - return result; - }; + if (!Number.isInteger(frame)) { + throw new ArgumentError('The frame argument must be an integer'); + } - Job.prototype.annotations.select.implementation = function (objectStates, x, y) { - return getCollection(this).select(objectStates, x, y); - }; + if (frame < this.startFrame || frame > this.stopFrame) { + throw new ArgumentError(`Frame ${frame} does not exist in the job`); + } - Job.prototype.annotations.statistics.implementation = function () { - return getCollection(this).statistics(); - }; + const annotationsData = await getAnnotations(this, frame, allTracks, filters); + const deletedFrames = await getDeletedFrames('job', this.id); + if (frame in deletedFrames) { + return []; + } - Job.prototype.annotations.put.implementation = function (objectStates) { - return getCollection(this).put(objectStates); - }; + return annotationsData; + }, + }); + + Object.defineProperty(Job.prototype.annotations.search, 'implementation', { + value: function searchAnnotationsImplementation( + this: JobClass, + frameFrom: Parameters[0], + frameTo: Parameters[1], + searchParameters: Parameters[2], + ): ReturnType { + if ('annotationsFilters' in searchParameters && !Array.isArray(searchParameters.annotationsFilters)) { + throw new ArgumentError('Annotations filters must be an array'); + } - Job.prototype.annotations.upload.implementation = async function ( - format: string, - useDefaultLocation: boolean, - sourceStorage: Storage, - file: File | string, - options?: { convMaskToPoly?: boolean }, - ) { - const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file, options); - return result; - }; + if ('generalFilters' in searchParameters && typeof searchParameters.generalFilters.isEmptyFrame !== 'boolean') { + throw new ArgumentError('General filter isEmptyFrame must be a boolean'); + } - Job.prototype.annotations.import.implementation = function (data) { - return getCollection(this).import(data); - }; + if ('annotationsFilters' in searchParameters && 'generalFilters' in searchParameters) { + throw new ArgumentError('Both annotations filters and general fiters could not be used together'); + } - Job.prototype.annotations.export.implementation = function () { - return getCollection(this).export(); - }; + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - Job.prototype.annotations.exportDataset.implementation = async function ( - format: string, - saveImages: boolean, - useDefaultSettings: boolean, - targetStorage: Storage, - customName?: string, - ) { - const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); - return result; - }; + if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { + throw new ArgumentError('The start frame is out of the job'); + } - Job.prototype.actions.undo.implementation = async function (count) { - return getHistory(this).undo(count); - }; + if (frameTo < this.startFrame || frameTo > this.stopFrame) { + throw new ArgumentError('The stop frame is out of the job'); + } - Job.prototype.actions.redo.implementation = async function (count) { - return getHistory(this).redo(count); - }; + return Promise.resolve( + getCollection(this).search(frameFrom, frameTo, searchParameters), + ); + }, + }); + + Object.defineProperty(Job.prototype.annotations.save, 'implementation', { + value: async function saveAnnotationsImplementation( + this: JobClass, + onUpdate: Parameters[0], + ): ReturnType { + return getSaver(this).save(onUpdate); + }, + }); + + Object.defineProperty(Job.prototype.annotations.merge, 'implementation', { + value: function mergeAnnotationsImplementation( + this: JobClass, + objectStates: Parameters[0], + ): ReturnType { + return Promise.resolve(getCollection(this).merge(objectStates)); + }, + }); + + Object.defineProperty(Job.prototype.annotations.split, 'implementation', { + value: function splitAnnotationsImplementation( + this: JobClass, + objectState: Parameters[0], + frame: Parameters[1], + ): ReturnType { + return Promise.resolve(getCollection(this).split(objectState, frame)); + }, + }); + + Object.defineProperty(Job.prototype.annotations.group, 'implementation', { + value: function groupAnnotationsImplementation( + this: JobClass, + objectStates: Parameters[0], + reset: Parameters[1], + ): ReturnType { + return Promise.resolve(getCollection(this).group(objectStates, reset)); + }, + }); + + Object.defineProperty(Job.prototype.annotations.join, 'implementation', { + value: function joinAnnotationsImplementation( + this: JobClass, + objectStates: Parameters[0], + points: Parameters[1], + ): ReturnType { + return Promise.resolve(getCollection(this).join(objectStates, points)); + }, + }); + + Object.defineProperty(Job.prototype.annotations.slice, 'implementation', { + value: function sliceAnnotationsImplementation( + this: JobClass, + objectState: Parameters[0], + results: Parameters[1], + ): ReturnType { + return Promise.resolve(getCollection(this).slice(objectState, results)); + }, + }); + + Object.defineProperty(Job.prototype.annotations.hasUnsavedChanges, 'implementation', { + value: function hasUnsavedChangesImplementation( + this: JobClass, + ): ReturnType { + return getSaver(this).hasUnsavedChanges(); + }, + }); + + Object.defineProperty(Job.prototype.annotations.clear, 'implementation', { + value: function clearAnnotationsImplementation( + this: JobClass, + options: Parameters[0], + ): ReturnType { + return clearAnnotations(this, options); + }, + }); + + Object.defineProperty(Job.prototype.annotations.select, 'implementation', { + value: function selectAnnotationsImplementation( + this: JobClass, + objectStates: Parameters[0], + x: Parameters[1], + y: Parameters[2], + ): ReturnType { + return Promise.resolve(getCollection(this).select(objectStates, x, y)); + }, + }); + + Object.defineProperty(Job.prototype.annotations.statistics, 'implementation', { + value: function statisticsImplementation( + this: JobClass, + ): ReturnType { + return Promise.resolve(getCollection(this).statistics()); + }, + }); + + Object.defineProperty(Job.prototype.annotations.put, 'implementation', { + value: function putAnnotationsImplementation( + this: JobClass, + objectStates: Parameters[0], + ): ReturnType { + return Promise.resolve(getCollection(this).put(objectStates)); + }, + }); + + Object.defineProperty(Job.prototype.annotations.import, 'implementation', { + value: function importAnnotationsImplementation( + this: JobClass, + data: Parameters[0], + ): ReturnType { + getCollection(this).import(data); + return Promise.resolve(); + }, + }); + + Object.defineProperty(Job.prototype.annotations.export, 'implementation', { + value: function exportAnnotationsImplementation( + this: JobClass, + ): ReturnType { + return Promise.resolve(getCollection(this).export()); + }, + }); + + Object.defineProperty(Job.prototype.annotations.upload, 'implementation', { + value: async function uploadAnnotationsImplementation( + this: JobClass, + format: Parameters[0], + useDefaultLocation: Parameters[1], + sourceStorage: Parameters[2], + file: Parameters[3], + options: Parameters[4], + ): ReturnType { + return importDataset(this, format, useDefaultLocation, sourceStorage, file, options); + }, + }); + + Object.defineProperty(Job.prototype.annotations.exportDataset, 'implementation', { + value: async function exportDatasetImplementation( + this: JobClass, + format: Parameters[0], + saveImages: Parameters[1], + useDefaultSettings: Parameters[2], + targetStorage: Parameters[3], + customName?: Parameters[4], + ): ReturnType { + return exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); + }, + }); + + Object.defineProperty(Job.prototype.actions.undo, 'implementation', { + value: async function undoActionImplementation( + this: JobClass, + count: Parameters[0], + ): ReturnType { + return getHistory(this).undo(count); + }, + }); + + Object.defineProperty(Job.prototype.actions.redo, 'implementation', { + value: async function redoActionImplementation( + this: JobClass, + count: Parameters[0], + ): ReturnType { + return getHistory(this).redo(count); + }, + }); + + Object.defineProperty(Job.prototype.actions.freeze, 'implementation', { + value: function freezeActionsImplementation( + this: JobClass, + frozen: Parameters[0], + ): ReturnType { + return Promise.resolve(getHistory(this).freeze(frozen)); + }, + }); + + Object.defineProperty(Job.prototype.actions.clear, 'implementation', { + value: function clearActionsImplementation( + this: JobClass, + ): ReturnType { + return Promise.resolve(getHistory(this).clear()); + }, + }); + + Object.defineProperty(Job.prototype.actions.get, 'implementation', { + value: function getActionsImplementation( + this: JobClass, + ): ReturnType { + return Promise.resolve(getHistory(this).get()); + }, + }); + + Object.defineProperty(Job.prototype.logger.log, 'implementation', { + value: async function logImplementation( + this: JobClass, + scope: Parameters[0], + payload: Parameters[1], + wait: Parameters[2], + ): ReturnType { + return logger.log( + scope, + { + ...payload, + project_id: this.projectId, + task_id: this.taskId, + job_id: this.id, + }, + wait, + ); + }, + }); - Job.prototype.actions.freeze.implementation = function (frozen) { - return getHistory(this).freeze(frozen); - }; + return Job; +} - Job.prototype.actions.clear.implementation = function () { - return getHistory(this).clear(); - }; +export function implementTask(Task: typeof TaskClass): typeof TaskClass { + Object.defineProperty(Task.prototype.close, 'implementation', { + value: function closeImplementation( + this: TaskClass, + ) { + for (const job of this.jobs) { + clearFrames(job.id); + clearCache(job); + } - Job.prototype.actions.get.implementation = function () { - return getHistory(this).get(); - }; + clearCache(this); + }, + }); - Job.prototype.logger.log.implementation = async function (scope, payload, wait) { - const result = await logger.log( - scope, - { - ...payload, - project_id: this.projectId, - task_id: this.taskId, - job_id: this.id, - }, - wait, - ); - return result; - }; + Object.defineProperty(Task.prototype.guide, 'implementation', { + value: async function guideImplementation( + this: TaskClass, + ): ReturnType { + if (this.guideId === null) { + return null; + } - Job.prototype.close.implementation = function closeTask() { - clearFrames(this.id); - clearCache(this); - return this; - }; + const result = await serverProxy.guides.get(this.guideId); + return new AnnotationGuide(result); + }, + }); + + Object.defineProperty(Task.prototype.save, 'implementation', { + value: async function saveImplementation( + this: TaskClass, + onUpdate: Parameters[0], + ): ReturnType { + if (typeof this.id !== 'undefined') { + // If the task has been already created, we update it + const taskData = this._updateTrigger.getUpdated(this, { + bugTracker: 'bug_tracker', + projectId: 'project_id', + assignee: 'assignee_id', + }); + + if (taskData.assignee_id) { + taskData.assignee_id = taskData.assignee_id.id; + } - Job.prototype.guide.implementation = async function guide() { - if (this.guideId === null) { - return null; - } + for await (const label of taskData.labels || []) { + if (label.deleted) { + await serverProxy.labels.delete(label.id); + } else if (label.patched) { + await serverProxy.labels.update(label.id, label.toJSON()); + } + } - const result = await serverProxy.guides.get(this.guideId); - return new AnnotationGuide(result); - }; + // leave only new labels to create them via task PATCH request + taskData.labels = (taskData.labels || []) + .filter((label: SerializedLabel) => !Number.isInteger(label.id)).map((el) => el.toJSON()); + if (!taskData.labels.length) { + delete taskData.labels; + } - return Job; -} + this._updateTrigger.reset(); -export function implementTask(Task) { - Task.prototype.close.implementation = function closeTask() { - for (const job of this.jobs) { - clearFrames(job.id); - clearCache(job); - } + let serializedTask: SerializedTask = null; + if (Object.keys(taskData).length) { + serializedTask = await serverProxy.tasks.save(this.id, taskData); + } else { + [serializedTask] = (await serverProxy.tasks.get({ id: this.id })); + } - clearCache(this); - return this; - }; + const labels = await serverProxy.labels.get({ task_id: this.id }); + const jobs = await serverProxy.jobs.get({ task_id: this.id }, true); + return new Task({ + ...omit(serializedTask, ['jobs', 'labels']), + progress: serializedTask.jobs, + jobs, + labels: labels.results, + }); + } - Task.prototype.save.implementation = async function (onUpdate) { - if (typeof this.id !== 'undefined') { - // If the task has been already created, we update it - const taskData = this._updateTrigger.getUpdated(this, { - bugTracker: 'bug_tracker', - projectId: 'project_id', - assignee: 'assignee_id', - }); + const taskSpec: any = { + name: this.name, + labels: this.labels.map((el) => el.toJSON()), + }; - if (taskData.assignee_id) { - taskData.assignee_id = taskData.assignee_id.id; + if (typeof this.bugTracker !== 'undefined') { + taskSpec.bug_tracker = this.bugTracker; + } + if (typeof this.segmentSize !== 'undefined') { + taskSpec.segment_size = this.segmentSize; + } + if (typeof this.overlap !== 'undefined') { + taskSpec.overlap = this.overlap; + } + if (typeof this.projectId !== 'undefined') { + taskSpec.project_id = this.projectId; + } + if (typeof this.subset !== 'undefined') { + taskSpec.subset = this.subset; } - for await (const label of taskData.labels || []) { - if (label.deleted) { - await serverProxy.labels.delete(label.id); - } else if (label.patched) { - await serverProxy.labels.update(label.id, label.toJSON()); - } + if (this.targetStorage) { + taskSpec.target_storage = this.targetStorage.toJSON(); } - // leave only new labels to create them via task PATCH request - taskData.labels = (taskData.labels || []) - .filter((label: SerializedLabel) => !Number.isInteger(label.id)).map((el) => el.toJSON()); - if (!taskData.labels.length) { - delete taskData.labels; + if (this.sourceStorage) { + taskSpec.source_storage = this.sourceStorage.toJSON(); } - this._updateTrigger.reset(); + const taskDataSpec = { + client_files: this.clientFiles, + server_files: this.serverFiles, + remote_files: this.remoteFiles, + image_quality: this.imageQuality, + use_zip_chunks: this.useZipChunks, + use_cache: this.useCache, + sorting_method: this.sortingMethod, + ...(typeof this.startFrame !== 'undefined' ? { start_frame: this.startFrame } : {}), + ...(typeof this.stopFrame !== 'undefined' ? { stop_frame: this.stopFrame } : {}), + ...(typeof this.frameFilter !== 'undefined' ? { frame_filter: this.frameFilter } : {}), + ...(typeof this.dataChunkSize !== 'undefined' ? { chunk_size: this.dataChunkSize } : {}), + ...(typeof this.copyData !== 'undefined' ? { copy_data: this.copyData } : {}), + ...(typeof this.cloudStorageId !== 'undefined' ? { cloud_storage_id: this.cloudStorageId } : {}), + }; + + const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate); + const labels = await serverProxy.labels.get({ task_id: task.id }); + const jobs = await serverProxy.jobs.get({ + filter: JSON.stringify({ and: [{ '==': [{ var: 'task_id' }, task.id] }] }), + }, true); - let serializedTask: SerializedTask = null; - if (Object.keys(taskData).length) { - serializedTask = await serverProxy.tasks.save(this.id, taskData); - } else { - [serializedTask] = (await serverProxy.tasks.get({ id: this.id })); + return new Task({ + ...omit(task, ['jobs', 'labels']), + jobs, + progress: task.jobs, + labels: labels.results, + }); + }, + }); + + Object.defineProperty(Task.prototype.listenToCreate, 'implementation', { + value: async function listenToCreateImplementation( + this: TaskClass, + onUpdate: Parameters[0], + ): ReturnType { + if (Number.isInteger(this.id) && this.size === 0) { + const serializedTask = await serverProxy.tasks.listenToCreate(this.id, onUpdate); + return new Task(omit(serializedTask, ['labels', 'jobs'])); } - const labels = await serverProxy.labels.get({ task_id: this.id }); - const jobs = await serverProxy.jobs.get({ task_id: this.id }, true); + return this; + }, + }); + + Object.defineProperty(Task.prototype.delete, 'implementation', { + value: function deleteImplementation( + this: TaskClass, + ): ReturnType { + return serverProxy.tasks.delete(this.id); + }, + }); + + Object.defineProperty(Task.prototype.issues, 'implementation', { + value: function issuesImplementation( + this: TaskClass, + ): ReturnType { + return serverProxy.issues.get({ task_id: this.id }) + .then((issues) => issues.map((issue) => new Issue(issue))); + }, + }); + + Object.defineProperty(Task.prototype.backup, 'implementation', { + value: function backupImplementation( + this: TaskClass, + targetStorage: Parameters[0], + useDefaultSettings: Parameters[1], + fileName: Parameters[2], + ): ReturnType { + return serverProxy.tasks.backup(this.id, targetStorage, useDefaultSettings, fileName); + }, + }); + + Object.defineProperty(Task.restore, 'implementation', { + value: async function restoreImplementation( + this: TaskClass, + storage: Parameters[0], + file: Parameters[1], + ): ReturnType { + const serializedTask = await serverProxy.tasks.restore(storage, file); + // When request task by ID we also need to add labels and jobs to work with them + const labels = await serverProxy.labels.get({ task_id: serializedTask.id }); + const jobs = await serverProxy.jobs.get({ task_id: serializedTask.id }, true); return new Task({ ...omit(serializedTask, ['jobs', 'labels']), progress: serializedTask.jobs, jobs, labels: labels.results, }); - } - - const taskSpec: any = { - name: this.name, - labels: this.labels.map((el) => el.toJSON()), - }; - - if (typeof this.bugTracker !== 'undefined') { - taskSpec.bug_tracker = this.bugTracker; - } - if (typeof this.segmentSize !== 'undefined') { - taskSpec.segment_size = this.segmentSize; - } - if (typeof this.overlap !== 'undefined') { - taskSpec.overlap = this.overlap; - } - if (typeof this.projectId !== 'undefined') { - taskSpec.project_id = this.projectId; - } - if (typeof this.subset !== 'undefined') { - taskSpec.subset = this.subset; - } - - if (this.targetStorage) { - taskSpec.target_storage = this.targetStorage.toJSON(); - } - - if (this.sourceStorage) { - taskSpec.source_storage = this.sourceStorage.toJSON(); - } - - const taskDataSpec = { - client_files: this.clientFiles, - server_files: this.serverFiles, - remote_files: this.remoteFiles, - image_quality: this.imageQuality, - use_zip_chunks: this.useZipChunks, - use_cache: this.useCache, - sorting_method: this.sortingMethod, - ...(typeof this.startFrame !== 'undefined' ? { start_frame: this.startFrame } : {}), - ...(typeof this.stopFrame !== 'undefined' ? { stop_frame: this.stopFrame } : {}), - ...(typeof this.frameFilter !== 'undefined' ? { frame_filter: this.frameFilter } : {}), - ...(typeof this.dataChunkSize !== 'undefined' ? { chunk_size: this.dataChunkSize } : {}), - ...(typeof this.copyData !== 'undefined' ? { copy_data: this.copyData } : {}), - ...(typeof this.cloudStorageId !== 'undefined' ? { cloud_storage_id: this.cloudStorageId } : {}), - }; - - const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate); - const labels = await serverProxy.labels.get({ task_id: task.id }); - const jobs = await serverProxy.jobs.get({ - filter: JSON.stringify({ and: [{ '==': [{ var: 'task_id' }, task.id] }] }), - }, true); - - return new Task({ - ...omit(task, ['jobs', 'labels']), - jobs, - progress: task.jobs, - labels: labels.results, - }); - }; - - Task.prototype.listenToCreate.implementation = async function ( - onUpdate: (state: RQStatus, progress: number, message: string) => void = () => {}, - ): Promise { - if (Number.isInteger(this.id) && this.size === 0) { - const serializedTask = await serverProxy.tasks.listenToCreate(this.id, onUpdate); - return new Task(omit(serializedTask, ['labels', 'jobs'])); - } - - return this; - }; - - Task.prototype.delete.implementation = async function () { - const result = await serverProxy.tasks.delete(this.id); - return result; - }; - - Task.prototype.issues.implementation = async function () { - const result = await serverProxy.issues.get({ task_id: this.id }); - return result.map((issue) => new Issue(issue)); - }; - - Task.prototype.backup.implementation = async function ( - targetStorage: Storage, - useDefaultSettings: boolean, - fileName?: string, - ) { - const result = await serverProxy.tasks.backup(this.id, targetStorage, useDefaultSettings, fileName); - return result; - }; - - Task.restore.implementation = async function (storage: Storage, file: File | string) { - // eslint-disable-next-line no-unsanitized/method - const result = await serverProxy.tasks.restore(storage, file); - return result; - }; - - Task.prototype.frames.get.implementation = async function (frame, isPlaying, step) { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } - - if (frame >= this.size) { - throw new ArgumentError(`The frame with number ${frame} is out of the task`); - } - - const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; - - const result = await getFrame( - job.id, - this.dataChunkSize, - this.dataChunkType, - this.mode, - frame, - job.startFrame, - job.stopFrame, - isPlaying, - step, - this.dimension, - (chunkNumber, quality) => job.frames.chunk(chunkNumber, quality), - ); - return result; - }; - - Task.prototype.frames.cachedChunks.implementation = async function () { - let chunks = []; - for (const job of this.jobs) { - const cachedChunks = await getCachedChunks(job.id); - chunks = chunks.concat(cachedChunks); - } - return Array.from(new Set(chunks)); - }; - - Task.prototype.frames.preview.implementation = async function (this: TaskClass): Promise { - if (this.id === null) return ''; - const preview = await serverProxy.tasks.getPreview(this.id); - if (!preview) return ''; - return decodePreview(preview); - }; - - Task.prototype.frames.delete.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } - - if (frame < 0 || frame >= this.size) { - throw new Error('The frame is out of the task'); - } - - const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; - if (job) { - await deleteFrameWrapper.call(this, job.id, frame); - } - }; - - Task.prototype.frames.restore.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } - - if (frame < 0 || frame >= this.size) { - throw new Error('The frame is out of the task'); - } - - const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; - if (job) { - await restoreFrameWrapper.call(this, job.id, frame); - } - }; - - Task.prototype.frames.save.implementation = async function () { - return Promise.all(this.jobs.map((job) => patchMeta(job.id))); - }; - - Task.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { - if (typeof filters !== 'object') { - throw new ArgumentError('Filters should be an object'); - } - - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } - - if (frameFrom < 0 || frameFrom > this.size) { - throw new ArgumentError('The start frame is out of the task'); - } - - if (frameTo < 0 || frameTo > this.size) { - throw new ArgumentError('The stop frame is out of the task'); - } + }, + }); + + Object.defineProperty(Task.prototype.frames.get, 'implementation', { + value: async function getFrameImplementation( + this: TaskClass, + frame: Parameters[0], + isPlaying: Parameters[1], + step: Parameters[2], + ): ReturnType { + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + } - const jobs = this.jobs.filter((_job) => ( - (frameFrom >= _job.startFrame && frameFrom <= _job.stopFrame) || - (frameTo >= _job.startFrame && frameTo <= _job.stopFrame) || - (frameFrom < _job.startFrame && frameTo > _job.stopFrame) - )); + if (frame >= this.size) { + throw new ArgumentError(`The frame with number ${frame} is out of the task`); + } - for (const job of jobs) { - const result = await findFrame( - job.id, Math.max(frameFrom, job.startFrame), Math.min(frameTo, job.stopFrame), filters, + const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; + + const result = await getFrame( + job.id, + this.dataChunkSize, + this.dataChunkType, + this.mode, + frame, + job.startFrame, + job.stopFrame, + isPlaying, + step, + this.dimension, + (chunkNumber, quality) => job.frames.chunk(chunkNumber, quality), ); + return result; + }, + }); + + Object.defineProperty(Task.prototype.frames.cachedChunks, 'implementation', { + value: async function cachedChunksImplementation( + this: TaskClass, + ): ReturnType { + throw new Error('Not implemented for Task'); + }, + }); + + Object.defineProperty(Task.prototype.frames.preview, 'implementation', { + value: function previewImplementation( + this: TaskClass, + ): ReturnType { + if (this.id === null) { + return Promise.resolve(''); + } - if (result !== null) return result; - } - - return null; - }; - - Task.prototype.frames.contextImage.implementation = async function () { - throw new Error('Not implemented'); - }; - - Task.prototype.frames.chunk.implementation = async function () { - throw new Error('Not implemented'); - }; - - // TODO: Check filter for annotations - Task.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { - if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { - throw new ArgumentError('The filters argument must be an array of strings'); - } - - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } - - if (frame >= this.size) { - throw new ArgumentError(`Frame ${frame} does not exist in the task`); - } - - const result = await getAnnotations(this, frame, allTracks, filters); - const deletedFrames = await getDeletedFrames('task', this.id); - if (frame in deletedFrames) { - return []; - } - - return result; - }; - - Task.prototype.annotations.search.implementation = function (frameFrom, frameTo, searchParameters) { - if ('annotationsFilters' in searchParameters && !Array.isArray(searchParameters.annotationsFilters)) { - throw new ArgumentError('Annotations filters must be an array'); - } - - if ('generalFilters' in searchParameters && typeof searchParameters.generalFilters.isEmptyFrame !== 'boolean') { - throw new ArgumentError('General filter isEmptyFrame must be a boolean'); - } - - if ('annotationsFilters' in searchParameters && 'generalFilters' in searchParameters) { - throw new ArgumentError('Both annotations filters and general fiters could not be used together'); - } - - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } - - if (frameFrom < 0 || frameFrom >= this.size) { - throw new ArgumentError('The start frame is out of the task'); - } - - if (frameTo < 0 || frameTo >= this.size) { - throw new ArgumentError('The stop frame is out of the task'); - } - - return getCollection(this).search(frameFrom, frameTo, searchParameters); - }; - - Task.prototype.annotations.save.implementation = async function (onUpdate) { - return getSaver(this).save(onUpdate); - }; - - Task.prototype.annotations.merge.implementation = async function (objectStates) { - return getCollection(this).merge(objectStates); - }; + return serverProxy.tasks.getPreview(this.id).then((preview) => { + if (!preview) { + return Promise.resolve(''); + } + return decodePreview(preview); + }); + }, + }); + + Object.defineProperty(Task.prototype.frames.delete, 'implementation', { + value: async function deleteFrameImplementation( + this: TaskClass, + frame: Parameters[0], + ): ReturnType { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } - Task.prototype.annotations.split.implementation = async function (objectState, frame) { - return getCollection(this).split(objectState, frame); - }; + if (frame < 0 || frame >= this.size) { + throw new Error('The frame is out of the task'); + } - Task.prototype.annotations.group.implementation = async function (objectStates, reset) { - return getCollection(this).group(objectStates, reset); - }; + const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; + if (job) { + await deleteFrameWrapper.call(this, job.id, frame); + } + }, + }); + + Object.defineProperty(Task.prototype.frames.restore, 'implementation', { + value: async function restoreFrameImplementation( + this: TaskClass, + frame: Parameters[0], + ): ReturnType { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } - Task.prototype.annotations.join.implementation = async function (objectStates, points) { - return getCollection(this).join(objectStates, points); - }; + if (frame < 0 || frame >= this.size) { + throw new Error('The frame is out of the task'); + } - Task.prototype.annotations.slice.implementation = async function (objectState, results) { - return getCollection(this).slice(objectState, results); - }; + const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; + if (job) { + await restoreFrameWrapper.call(this, job.id, frame); + } + }, + }); + + Object.defineProperty(Task.prototype.frames.save, 'implementation', { + value: async function saveFramesImplementation( + this: TaskClass, + ): ReturnType { + return Promise.all(this.jobs.map((job) => patchMeta(job.id))) + .then(() => Promise.resolve()); + }, + }); + + Object.defineProperty(Task.prototype.frames.search, 'implementation', { + value: async function searchFrameImplementation( + this: TaskClass, + filters: Parameters[0], + frameFrom: Parameters[1], + frameTo: Parameters[2], + ): ReturnType { + if (typeof filters !== 'object') { + throw new ArgumentError('Filters should be an object'); + } - Task.prototype.annotations.hasUnsavedChanges.implementation = function () { - return getSaver(this).hasUnsavedChanges(); - }; + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - Task.prototype.annotations.clear.implementation = async function ( - reload, startframe, endframe, delTrackKeyframesOnly, - ) { - const result = await clearAnnotations(this, reload, startframe, endframe, delTrackKeyframesOnly); - return result; - }; + if (frameFrom < 0 || frameFrom > this.size) { + throw new ArgumentError('The start frame is out of the task'); + } - Task.prototype.annotations.select.implementation = function (objectStates, x, y) { - return getCollection(this).select(objectStates, x, y); - }; + if (frameTo < 0 || frameTo > this.size) { + throw new ArgumentError('The stop frame is out of the task'); + } - Task.prototype.annotations.statistics.implementation = function () { - return getCollection(this).statistics(); - }; + const jobs = this.jobs.filter((_job) => ( + (frameFrom >= _job.startFrame && frameFrom <= _job.stopFrame) || + (frameTo >= _job.startFrame && frameTo <= _job.stopFrame) || + (frameFrom < _job.startFrame && frameTo > _job.stopFrame) + )); - Task.prototype.annotations.put.implementation = function (objectStates) { - return getCollection(this).put(objectStates); - }; + for (const job of jobs) { + const result = await findFrame( + job.id, Math.max(frameFrom, job.startFrame), Math.min(frameTo, job.stopFrame), filters, + ); - Task.prototype.annotations.upload.implementation = async function ( - format: string, - useDefaultLocation: boolean, - sourceStorage: Storage, - file: File | string, - options?: { convMaskToPoly?: boolean }, - ) { - const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file, options); - return result; - }; + if (result !== null) return result; + } - Task.prototype.annotations.import.implementation = function (data) { - return getCollection(this).import(data); - }; + return null; + }, + }); + + Object.defineProperty(Task.prototype.frames.contextImage, 'implementation', { + value: function contextImageImplementation( + this: TaskClass, + ): ReturnType { + throw new Error('Not implemented for Task'); + }, + }); + + Object.defineProperty(Task.prototype.frames.chunk, 'implementation', { + value: function chunkImplementation( + this: TaskClass, + ): ReturnType { + throw new Error('Not implemented for Task'); + }, + }); + + Object.defineProperty(Task.prototype.annotations.get, 'implementation', { + value: async function getAnnotationsImplementation( + this: TaskClass, + frame: Parameters[0], + allTracks: Parameters[1], + filters: Parameters[2], + ): ReturnType { + if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { + throw new ArgumentError('The filters argument must be an array of strings'); + } - Task.prototype.annotations.export.implementation = function () { - return getCollection(this).export(); - }; + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + } - Task.prototype.annotations.exportDataset.implementation = async function ( - format: string, - saveImages: boolean, - useDefaultSettings: boolean, - targetStorage: Storage, - customName?: string, - ) { - const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); - return result; - }; + if (frame >= this.size) { + throw new ArgumentError(`Frame ${frame} does not exist in the task`); + } - Task.prototype.actions.undo.implementation = async function (count) { - return getHistory(this).undo(count); - }; + const result = await getAnnotations(this, frame, allTracks, filters); + const deletedFrames = await getDeletedFrames('task', this.id); + if (frame in deletedFrames) { + return []; + } - Task.prototype.actions.redo.implementation = async function (count) { - return getHistory(this).redo(count); - }; + return result; + }, + }); + + Object.defineProperty(Task.prototype.annotations.search, 'implementation', { + value: function searchAnnotationsImplementation( + this: TaskClass, + frameFrom: Parameters[0], + frameTo: Parameters[1], + searchParameters: Parameters[2], + ): ReturnType { + if ('annotationsFilters' in searchParameters && !Array.isArray(searchParameters.annotationsFilters)) { + throw new ArgumentError('Annotations filters must be an array'); + } - Task.prototype.actions.freeze.implementation = function (frozen) { - return getHistory(this).freeze(frozen); - }; + if ('generalFilters' in searchParameters && typeof searchParameters.generalFilters.isEmptyFrame !== 'boolean') { + throw new ArgumentError('General filter isEmptyFrame must be a boolean'); + } - Task.prototype.actions.clear.implementation = function () { - return getHistory(this).clear(); - }; + if ('annotationsFilters' in searchParameters && 'generalFilters' in searchParameters) { + throw new ArgumentError('Both annotations filters and general fiters could not be used together'); + } - Task.prototype.actions.get.implementation = function () { - return getHistory(this).get(); - }; + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - Task.prototype.logger.log.implementation = async function (scope, payload, wait) { - const result = await logger.log( - scope, - { - ...payload, - project_id: this.projectId, - task_id: this.id, - }, - wait, - ); - return result; - }; + if (frameFrom < 0 || frameFrom >= this.size) { + throw new ArgumentError('The start frame is out of the task'); + } - Task.prototype.guide.implementation = async function guide() { - if (this.guideId === null) { - return null; - } + if (frameTo < 0 || frameTo >= this.size) { + throw new ArgumentError('The stop frame is out of the task'); + } - const result = await serverProxy.guides.get(this.guideId); - return new AnnotationGuide(result); - }; + return Promise.resolve(getCollection(this).search(frameFrom, frameTo, searchParameters)); + }, + }); + + Object.defineProperty(Task.prototype.annotations.save, 'implementation', { + value: function saveAnnotationsImplementation( + this: TaskClass, + onUpdate: Parameters[0], + ): ReturnType { + return getSaver(this).save(onUpdate); + }, + }); + + Object.defineProperty(Task.prototype.annotations.merge, 'implementation', { + value: function mergeAnnotationsImplementation( + this: TaskClass, + objectStates: Parameters[0], + ): ReturnType { + return Promise.resolve(getCollection(this).merge(objectStates)); + }, + }); + + Object.defineProperty(Task.prototype.annotations.split, 'implementation', { + value: function splitAnnotationsImplementation( + this: TaskClass, + objectState: Parameters[0], + frame: Parameters[1], + ): ReturnType { + return Promise.resolve(getCollection(this).split(objectState, frame)); + }, + }); + + Object.defineProperty(Task.prototype.annotations.group, 'implementation', { + value: function groupAnnotationsImplementation( + this: TaskClass, + objectStates: Parameters[0], + reset: Parameters[1], + ): ReturnType { + return Promise.resolve(getCollection(this).group(objectStates, reset)); + }, + }); + + Object.defineProperty(Task.prototype.annotations.join, 'implementation', { + value: function joinAnnotationsImplementation( + this: TaskClass, + objectStates: Parameters[0], + points: Parameters[1], + ): ReturnType { + return Promise.resolve(getCollection(this).join(objectStates, points)); + }, + }); + + Object.defineProperty(Task.prototype.annotations.slice, 'implementation', { + value: function sliceAnnotationsImplementation( + this: TaskClass, + objectState: Parameters[0], + results: Parameters[1], + ): ReturnType { + return Promise.resolve(getCollection(this).slice(objectState, results)); + }, + }); + + Object.defineProperty(Task.prototype.annotations.hasUnsavedChanges, 'implementation', { + value: function hasUnsavedChangesImplementation( + this: TaskClass, + ): ReturnType { + return getSaver(this).hasUnsavedChanges(); + }, + }); + + Object.defineProperty(Task.prototype.annotations.clear, 'implementation', { + value: function clearAnnotationsImplementation( + this: TaskClass, + options: Parameters[0], + ): ReturnType { + return clearAnnotations(this, options); + }, + }); + + Object.defineProperty(Task.prototype.annotations.select, 'implementation', { + value: function selectAnnotationsImplementation( + this: TaskClass, + objectStates: Parameters[0], + x: Parameters[1], + y: Parameters[2], + ): ReturnType { + return Promise.resolve(getCollection(this).select(objectStates, x, y)); + }, + }); + + Object.defineProperty(Task.prototype.annotations.statistics, 'implementation', { + value: function statisticsImplementation( + this: TaskClass, + ): ReturnType { + return Promise.resolve(getCollection(this).statistics()); + }, + }); + + Object.defineProperty(Task.prototype.annotations.put, 'implementation', { + value: function putAnnotationsImplementation( + this: TaskClass, + objectStates: Parameters[0], + ): ReturnType { + return Promise.resolve(getCollection(this).put(objectStates)); + }, + }); + + Object.defineProperty(Task.prototype.annotations.upload, 'implementation', { + value: function uploadAnnotationsImplementation( + this: TaskClass, + format: Parameters[0], + useDefaultLocation: Parameters[1], + sourceStorage: Parameters[2], + file: Parameters[3], + options: Parameters[4], + ): ReturnType { + return importDataset(this, format, useDefaultLocation, sourceStorage, file, options); + }, + }); + + Object.defineProperty(Task.prototype.annotations.import, 'implementation', { + value: function importAnnotationsImplementation( + this: TaskClass, + data: Parameters[0], + ): ReturnType { + getCollection(this).import(data); + return Promise.resolve(); + }, + }); + + Object.defineProperty(Task.prototype.annotations.export, 'implementation', { + value: function exportAnnotationsImplementation( + this: TaskClass, + ): ReturnType { + return Promise.resolve(getCollection(this).export()); + }, + }); + + Object.defineProperty(Task.prototype.annotations.exportDataset, 'implementation', { + value: function exportDatasetImplementation( + this: TaskClass, + format: Parameters[0], + saveImages: Parameters[1], + useDefaultSettings: Parameters[2], + targetStorage: Parameters[3], + customName: Parameters[4], + ): ReturnType { + return exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); + }, + }); + + Object.defineProperty(Task.prototype.actions.undo, 'implementation', { + value: function undoActionImplementation( + this: TaskClass, + count: Parameters[0], + ): ReturnType { + return getHistory(this).undo(count); + }, + }); + + Object.defineProperty(Task.prototype.actions.redo, 'implementation', { + value: function redoActionImplementation( + this: TaskClass, + count: Parameters[0], + ): ReturnType { + return getHistory(this).redo(count); + }, + }); + + Object.defineProperty(Task.prototype.actions.freeze, 'implementation', { + value: function freezeActionsImplementation( + this: TaskClass, + frozen: Parameters[0], + ): ReturnType { + return Promise.resolve(getHistory(this).freeze(frozen)); + }, + }); + + Object.defineProperty(Task.prototype.actions.clear, 'implementation', { + value: function clearActionsImplementation( + this: TaskClass, + ): ReturnType { + return Promise.resolve(getHistory(this).clear()); + }, + }); + + Object.defineProperty(Task.prototype.actions.get, 'implementation', { + value: function getActionsImplementation( + this: TaskClass, + ): ReturnType { + return Promise.resolve(getHistory(this).get()); + }, + }); + + Object.defineProperty(Task.prototype.logger.log, 'implementation', { + value: function logImplementation( + this: TaskClass, + scope: Parameters[0], + payload: Parameters[1], + wait: Parameters[2], + ): ReturnType { + return logger.log( + scope, + { + ...payload, + project_id: this.projectId, + task_id: this.id, + }, + wait, + ); + }, + }); return Task; } diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 5565417ff9eb..701d05413cf9 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -4,8 +4,10 @@ // SPDX-License-Identifier: MIT import _ from 'lodash'; + +import { ChunkQuality } from 'cvat-data'; import { - ChunkType, DimensionType, JobStage, + ChunkType, DimensionType, HistoryActions, JobStage, JobState, JobType, RQStatus, StorageLocation, TaskMode, TaskStatus, } from './enums'; import { Storage } from './storage'; @@ -15,11 +17,16 @@ import { ArgumentError, ScriptingError } from './exceptions'; import { Label } from './labels'; import User from './user'; import { FieldUpdateTrigger } from './common'; -import { SerializedJob, SerializedLabel, SerializedTask } from './server-response-types'; +import { + SerializedCollection, SerializedJob, + SerializedLabel, SerializedTask, +} from './server-response-types'; import AnnotationGuide from './guide'; import { FrameData } from './frames'; import Statistics from './statistics'; import logger from './logger'; +import Issue from './issue'; +import ObjectState from './object-state'; function buildDuplicatedAPI(prototype) { Object.defineProperties(prototype, { @@ -49,11 +56,9 @@ function buildDuplicatedAPI(prototype) { return result; }, - async clear( - reload = false, startframe = undefined, endframe = undefined, delTrackKeyframesOnly = true, - ) { + async clear(options) { const result = await PluginRegistry.apiWrapper.call( - this, prototype.annotations.clear, reload, startframe, endframe, delTrackKeyframesOnly, + this, prototype.annotations.clear, options, ); return result; }, @@ -230,13 +235,13 @@ function buildDuplicatedAPI(prototype) { const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview); return result; }, - async search(filters, frameFrom, frameTo) { + async search(filters, startFrame, stopFrame) { const result = await PluginRegistry.apiWrapper.call( this, prototype.frames.search, filters, - frameFrom, - frameTo, + startFrame, + stopFrame, ); return result; }, @@ -305,43 +310,85 @@ function buildDuplicatedAPI(prototype) { export class Session { public annotations: { - get: CallableFunction; - put: CallableFunction; - save: CallableFunction; - merge: CallableFunction; - split: CallableFunction; - group: CallableFunction; - join: CallableFunction; - slice: CallableFunction; - clear: CallableFunction; - search: CallableFunction; - upload: CallableFunction; - select: CallableFunction; - import: CallableFunction; - export: CallableFunction; + get: (frame: number, allTracks: boolean, filters: object[]) => Promise; + put: (objectStates: ObjectState[]) => Promise; + merge: (objectStates: ObjectState[]) => Promise; + split: (objectState: ObjectState, frame: number) => Promise; + group: (objectStates: ObjectState[], reset: boolean) => Promise; + join: (objectStates: ObjectState[], points: number[]) => Promise; + slice: (state: ObjectState, results: number[][]) => Promise; + clear: (options?: { + reload?: boolean; + startFrame?: number; + stopFrame?: number; + delTrackKeyframesOnly?: boolean; + }) => Promise; + save: ( + onUpdate ?: (message: string) => void, + ) => Promise; + search: ( + frameFrom: number, + frameTo: number, + searchParameters: { + allowDeletedFrames: boolean; + annotationsFilters?: object[]; + generalFilters?: { + isEmptyFrame?: boolean; + }; + }, + ) => Promise; + upload: ( + format: string, + useDefaultSettings: boolean, + sourceStorage: Storage, + file: File | string, + options?: { + convMaskToPoly?: boolean, + updateStatusCallback?: (s: string, n: number) => void, + }, + ) => Promise; + select: (objectStates: ObjectState[], x: number, y: number) => Promise<{ + state: ObjectState, + distance: number | null, + }>; + import: (data: Omit) => Promise; + export: () => Promise>; statistics: () => Promise; - hasUnsavedChanges: CallableFunction; - exportDataset: CallableFunction; + hasUnsavedChanges: () => boolean; + exportDataset: ( + format: string, + saveImages: boolean, + useDefaultSettings: boolean, + targetStorage: Storage, + name?: string, + ) => Promise; }; public actions: { - undo: CallableFunction; - redo: CallableFunction; - freeze: CallableFunction; - clear: CallableFunction; - get: CallableFunction; + undo: (count: number) => Promise; + redo: (count: number) => Promise; + freeze: (frozen: boolean) => Promise; + clear: () => Promise; + get: () => Promise<{ undo: [HistoryActions, number][], redo: [HistoryActions, number][] }>; }; public frames: { get: (frame: number, isPlaying?: boolean, step?: number) => Promise; - delete: CallableFunction; - restore: CallableFunction; - save: CallableFunction; - cachedChunks: CallableFunction; - preview: CallableFunction; - contextImage: CallableFunction; - search: CallableFunction; - chunk: CallableFunction; + delete: (frame: number) => Promise; + restore: (frame: number) => Promise; + save: () => Promise; + cachedChunks: () => Promise; + preview: () => Promise; + contextImage: (frame: number) => Promise>; + search: ( + filters: { + offset?: number, + notDeleted: boolean, + }, + frameFrom: number, + frameTo: number, + ) => Promise; + chunk: (chunk: number, quality: ChunkQuality) => Promise; }; public logger: { @@ -610,12 +657,12 @@ export class Job extends Session { ); } - async save(additionalData = {}) { + async save(additionalData = {}): Promise { const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.save, additionalData); return result; } - async issues() { + async issues(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.issues); return result; } @@ -625,12 +672,12 @@ export class Job extends Session { return result; } - async openIssue(issue, message) { + async openIssue(issue: Issue, message: string): Promise { const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.openIssue, issue, message); return result; } - async close() { + async close(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.close); return result; } @@ -1060,12 +1107,12 @@ export class Task extends Session { ); } - async close(): Promise { + async close(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.close); return result; } - async save(onUpdate = () => {}): Promise { + async save(onUpdate: (state: RQStatus, progress: number, message: string) => void = () => {}): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.save, onUpdate); return result; } @@ -1082,7 +1129,7 @@ export class Task extends Session { return result; } - async backup(targetStorage: Storage, useDefaultSettings: boolean, fileName?: string) { + async backup(targetStorage: Storage, useDefaultSettings: boolean, fileName?: string): Promise { const result = await PluginRegistry.apiWrapper.call( this, Task.prototype.backup, @@ -1093,12 +1140,12 @@ export class Task extends Session { return result; } - async issues() { + async issues(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.issues); return result; } - static async restore(storage: Storage, file: File | string) { + static async restore(storage: Storage, file: File | string): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.restore, storage, file); return result; } diff --git a/cvat-core/tests/api/annotations.cjs b/cvat-core/tests/api/annotations.cjs index 724ecfa06c95..4b2bd4f7111d 100644 --- a/cvat-core/tests/api/annotations.cjs +++ b/cvat-core/tests/api/annotations.cjs @@ -247,7 +247,7 @@ describe('Feature: put annotations', () => { test('put object without objectType to a task', async () => { const task = (await cvat.tasks.get({ id: 101 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); expect(() => new cvat.classes.ObjectState({ frame: 1, shapeType: cvat.enums.ShapeType.POLYGON, @@ -260,7 +260,7 @@ describe('Feature: put annotations', () => { test('put shape with bad attributes to a task', async () => { const task = (await cvat.tasks.get({ id: 101 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); const state = new cvat.classes.ObjectState({ frame: 1, objectType: cvat.enums.ObjectType.SHAPE, @@ -277,7 +277,7 @@ describe('Feature: put annotations', () => { test('put shape with bad zOrder to a task', async () => { const task = (await cvat.tasks.get({ id: 101 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); const state = new cvat.classes.ObjectState({ frame: 1, objectType: cvat.enums.ObjectType.SHAPE, @@ -307,7 +307,7 @@ describe('Feature: put annotations', () => { test('put shape without points and with invalid points to a task', async () => { const task = (await cvat.tasks.get({ id: 101 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); const state = new cvat.classes.ObjectState({ frame: 1, objectType: cvat.enums.ObjectType.SHAPE, @@ -328,7 +328,7 @@ describe('Feature: put annotations', () => { test('put shape without type to a task', async () => { const task = (await cvat.tasks.get({ id: 101 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); const state = new cvat.classes.ObjectState({ frame: 1, objectType: cvat.enums.ObjectType.SHAPE, @@ -343,7 +343,7 @@ describe('Feature: put annotations', () => { test('put shape without label and with bad label to a task', async () => { const task = (await cvat.tasks.get({ id: 101 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); const state = { frame: 1, objectType: cvat.enums.ObjectType.SHAPE, @@ -363,7 +363,7 @@ describe('Feature: put annotations', () => { test('put shape with bad frame to a task', async () => { const task = (await cvat.tasks.get({ id: 101 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); expect(() => new cvat.classes.ObjectState({ frame: '5', objectType: cvat.enums.ObjectType.SHAPE, @@ -378,7 +378,7 @@ describe('Feature: put annotations', () => { test('put a skeleton shape to a job', async() => { const job = (await cvat.jobs.get({ jobID: 40 }))[0]; const label = job.labels[0]; - await job.annotations.clear(true); + await job.annotations.clear({ reload: true }); await job.annotations.clear(); const skeleton = new cvat.classes.ObjectState({ frame: 0, @@ -409,7 +409,7 @@ describe('Feature: put annotations', () => { test('put a skeleton track to a task', async() => { const task = (await cvat.tasks.get({ id: 40 }))[0]; const label = task.labels[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); await task.annotations.clear(); const skeleton = new cvat.classes.ObjectState({ frame: 0, @@ -769,7 +769,7 @@ describe('Feature: group annotations', () => { test('trying to group object state which has not been saved in a collection', async () => { const task = (await cvat.tasks.get({ id: 100 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); const state = new cvat.classes.ObjectState({ frame: 0, @@ -817,7 +817,7 @@ describe('Feature: clear annotations', () => { annotations[0].occluded = true; await annotations[0].save(); expect(task.annotations.hasUnsavedChanges()).toBe(true); - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); annotations = await task.annotations.get(0); expect(annotations).not.toHaveLength(0); expect(task.annotations.hasUnsavedChanges()).toBe(false); @@ -830,7 +830,7 @@ describe('Feature: clear annotations', () => { annotations[0].occluded = true; await annotations[0].save(); expect(job.annotations.hasUnsavedChanges()).toBe(true); - await job.annotations.clear(true); + await job.annotations.clear({ reload: true }); annotations = await job.annotations.get(0); expect(annotations).not.toHaveLength(0); expect(job.annotations.hasUnsavedChanges()).toBe(false); @@ -838,15 +838,15 @@ describe('Feature: clear annotations', () => { test('clear annotations with bad reload parameter', async () => { const task = (await cvat.tasks.get({ id: 100 }))[0]; - await task.annotations.clear(true); - expect(task.annotations.clear('reload')).rejects.toThrow(cvat.exceptions.ArgumentError); + await task.annotations.clear({ reload: true }); + expect(task.annotations.clear({ reload: 'reload' })).rejects.toThrow(cvat.exceptions.ArgumentError); }); }); describe('Feature: get statistics', () => { test('get statistics from a task', async () => { const task = (await cvat.tasks.get({ id: 100 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); const statistics = await task.annotations.statistics(); expect(statistics).toBeInstanceOf(cvat.classes.Statistics); expect(statistics.total.total).toBe(30); @@ -854,7 +854,7 @@ describe('Feature: get statistics', () => { test('get statistics from a job', async () => { const job = (await cvat.jobs.get({ jobID: 101 }))[0]; - await job.annotations.clear(true); + await job.annotations.clear({ reload: true }); const statistics = await job.annotations.statistics(); expect(statistics).toBeInstanceOf(cvat.classes.Statistics); expect(statistics.total.total).toBe(1012); @@ -862,7 +862,7 @@ describe('Feature: get statistics', () => { test('get statistics from a job with skeletons', async () => { const job = (await cvat.jobs.get({ jobID: 40 }))[0]; - await job.annotations.clear(true); + await job.annotations.clear({ reload: true }); const statistics = await job.annotations.statistics(); expect(statistics).toBeInstanceOf(cvat.classes.Statistics); expect(statistics.total.total).toBe(30); @@ -876,7 +876,7 @@ describe('Feature: get statistics', () => { test('get statistics from a job with skeletons', async () => { const job = (await cvat.jobs.get({ jobID: 102 }))[0]; - await job.annotations.clear(true); + await job.annotations.clear({ reload: true }); let statistics = await job.annotations.statistics(); expect(statistics.total.manually).toBe(5); expect(statistics.total.interpolated).toBe(443); @@ -955,7 +955,7 @@ describe('Feature: select object', () => { describe('Feature: search frame', () => { test('applying different filters', async () => { const job = (await cvat.jobs.get({ jobID: 102 }))[0]; - await job.annotations.clear(true); + await job.annotations.clear({ reload: true }); let frame = await job.annotations.search(495, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"type"},"tag"]}]}]') }); expect(frame).toBe(500); frame = await job.annotations.search(495, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"type"},"tag"]},{"==":[{"var":"label"},"bicycle"]}]}]') }); diff --git a/cvat-core/tests/api/frames.cjs b/cvat-core/tests/api/frames.cjs index 411cd3089916..5ba10a62dc04 100644 --- a/cvat-core/tests/api/frames.cjs +++ b/cvat-core/tests/api/frames.cjs @@ -47,7 +47,7 @@ describe('Feature: get frame meta', () => { describe('Feature: delete/restore frame', () => { test('delete frame from job', async () => { const job = (await cvat.jobs.get({ jobID: 100 }))[0]; - await job.annotations.clear(true); + await job.annotations.clear({ reload: true }); let frame = await job.frames.get(0); expect(frame.deleted).toBe(false); await job.frames.delete(0); @@ -57,7 +57,7 @@ describe('Feature: delete/restore frame', () => { test('restore frame from job', async () => { const job = (await cvat.jobs.get({ jobID: 100 }))[0]; - await job.annotations.clear(true); + await job.annotations.clear({ reload: true }); let frame = await job.frames.get(8); expect(frame.deleted).toBe(true); await job.frames.restore(8); @@ -67,7 +67,7 @@ describe('Feature: delete/restore frame', () => { test('delete frame from task', async () => { const task = (await cvat.tasks.get({ id: 100 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); let frame = await task.frames.get(1); expect(frame.deleted).toBe(false); await task.frames.delete(1); @@ -77,7 +77,7 @@ describe('Feature: delete/restore frame', () => { test('restore frame from task', async () => { const task = (await cvat.tasks.get({ id: 100 }))[0]; - await task.annotations.clear(true); + await task.annotations.clear({ reload: true }); let frame = await task.frames.get(7); expect(frame.deleted).toBe(true); await task.frames.restore(7); diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh index 5e7c0968e370..39d2a6bf6686 100755 --- a/cvat-sdk/gen/generate.sh +++ b/cvat-sdk/gen/generate.sh @@ -8,7 +8,7 @@ set -e GENERATOR_VERSION="v6.0.1" -VERSION="2.14.3" +VERSION="2.14.4" LIB_NAME="cvat_sdk" LAYER1_LIB_NAME="${LIB_NAME}/api_client" DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)" diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 3a821cd8380b..95e3002352e4 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -22,7 +22,7 @@ "dependencies": { "@ant-design/compatible": "^5.1.2", "@ant-design/icons": "^4.6.3", - "@react-awesome-query-builder/antd": "^6.2.1", + "@react-awesome-query-builder/antd": "^6.5.2", "@types/json-logic-js": "^2.0.2", "@types/lru-cache": "^7.10.10", "@types/platform": "^1.3.4", diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 08ac435baade..ec8c007357f8 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -370,12 +370,17 @@ export function updateCanvasBrushTools(config: { } export function removeAnnotationsAsync( - startFrame: number, endFrame: number, delTrackKeyframesOnly: boolean, + startFrame: number, stopFrame: number, delTrackKeyframesOnly: boolean, ): ThunkAction { return async (dispatch: ActionCreator, getState: () => CombinedState): Promise => { try { const { jobInstance } = receiveAnnotationsParameters(); - await jobInstance.annotations.clear(false, startFrame, endFrame, delTrackKeyframesOnly); + await jobInstance.annotations.clear({ + reload: false, + startFrame, + stopFrame, + delTrackKeyframesOnly, + }); await jobInstance.actions.clear(); dispatch(fetchAnnotationsAsync()); @@ -511,14 +516,14 @@ export function propagateObjectAsync(from: number, to: number): ThunkAction { }; } -export function removeObjectAsync(sessionInstance: NonNullable, objectState: any, force: boolean): ThunkAction { +export function removeObjectAsync(objectState: ObjectState, force: boolean): ThunkAction { return async (dispatch: ActionCreator): Promise => { try { - await sessionInstance.logger.log(EventScope.deleteObject, { count: 1 }); - const { frame } = receiveAnnotationsParameters(); + const { frame, jobInstance } = receiveAnnotationsParameters(); + await jobInstance.logger.log(EventScope.deleteObject, { count: 1 }); const removed = await objectState.delete(frame, force); - const history = await sessionInstance.actions.get(); + const history = await jobInstance.actions.get(); if (removed) { dispatch({ @@ -945,7 +950,7 @@ export function getJobAsync({ // frame query parameter does not work for GT job const frameNumber = Number.isInteger(initialFrame) && gtJob?.id !== job.id ? - initialFrame : (await job.frames.search( + initialFrame as number : (await job.frames.search( { notDeleted: !showDeletedFrames }, job.startFrame, job.stopFrame, )) || job.startFrame; @@ -964,7 +969,7 @@ export function getJobAsync({ let groundTruthJobFramesMeta = null; if (gtJob) { - gtJob.annotations.clear(true); // fetch gt annotations from the server + await gtJob.annotations.clear({ reload: true }); // fetch gt annotations from the server groundTruthJobFramesMeta = await cvat.frames.getMeta('job', gtJob.id); } diff --git a/cvat-ui/src/actions/import-actions.ts b/cvat-ui/src/actions/import-actions.ts index 5f5ba82a789f..1dd230b81cca 100644 --- a/cvat-ui/src/actions/import-actions.ts +++ b/cvat-ui/src/actions/import-actions.ts @@ -112,7 +112,7 @@ export const importDatasetAsync = ( await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file, { convMaskToPoly }); await instance.logger.log(EventScope.uploadAnnotations); - await instance.annotations.clear(true); + await instance.annotations.clear({ reload: true }); await instance.actions.clear(); // first set empty objects list diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx index 6cac7eaeb7d7..531be46b0299 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx @@ -802,7 +802,7 @@ class CanvasWrapperComponent extends React.PureComponent { jobInstance, activatedStateID, activatedElementID, workspace, onActivateObject, } = this.props; - if (![Workspace.STANDARD, Workspace.REVIEW].includes(workspace)) { + if (![Workspace.STANDARD, Workspace.REVIEW, Workspace.SINGLE_SHAPE].includes(workspace)) { return; } diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 45fa2910a957..c7d2f8ed3cc8 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -26,6 +26,7 @@ import { import { ActionUnion, createAction } from 'utils/redux'; import { rememberObject, changeFrameAsync, saveAnnotationsAsync, setNavigationType, + removeObjectAsync, } from 'actions/annotation-actions'; import LabelSelector from 'components/label-selector/label-selector'; import GlobalHotKeys from 'utils/mousetrap-react'; @@ -36,6 +37,43 @@ enum ReducerActionType { SWITCH_COUNT_OF_POINTS_IS_PREDEFINED = 'SWITCH_COUNT_OF_POINTS_IS_PREDEFINED', SET_ACTIVE_LABEL = 'SET_ACTIVE_LABEL', SET_POINTS_COUNT = 'SET_POINTS_COUNT', + SET_NEXT_FRAME = 'SET_NEXT_FRAME', +} + +function cancelCurrentCanvasOp(state: CombinedState): void { + const canvas = state.annotation.canvas.instance as Canvas; + if (canvas.mode() !== CanvasMode.IDLE) { + canvas.cancel(); + } +} + +function showSubmittedInfo(): void { + Modal.info({ + closable: false, + title: 'Annotations submitted', + content: 'You may close the window', + className: 'cvat-single-shape-annotation-submit-success-modal', + }); +} + +function makeMessage(label: Label, labelType: State['labelType'], pointsCount: number): JSX.Element { + let readableShape = ''; + if (labelType === LabelType.POINTS) { + readableShape = pointsCount === 1 ? 'one point' : `${pointsCount} points`; + } else if (labelType === LabelType.ELLIPSE) { + readableShape = 'an ellipse'; + } else { + readableShape = `a ${labelType}`; + } + + return ( + <> + Annotate + {` ${label.name} `} + on the image, using + {` ${readableShape} `} + + ); } export const reducerActions = { @@ -48,10 +86,15 @@ export const reducerActions = { switchCountOfPointsIsPredefined: () => ( createAction(ReducerActionType.SWITCH_COUNT_OF_POINTS_IS_PREDEFINED) ), - setActiveLabel: (label: Label, type?: LabelType) => ( + setActiveLabel: (label: Label, type: State['labelType']) => ( createAction(ReducerActionType.SET_ACTIVE_LABEL, { label, - labelType: type || label.type, + labelType: type, + }) + ), + setNextFrame: (nextFrame: number | null) => ( + createAction(ReducerActionType.SET_NEXT_FRAME, { + nextFrame, }) ), setPointsCount: (pointsCount: number) => ( @@ -61,12 +104,13 @@ export const reducerActions = { interface State { autoNextFrame: boolean; + nextFrame: number | null; saveOnFinish: boolean; pointsCountIsPredefined: boolean; pointsCount: number; labels: Label[]; label: Label | null; - labelType: LabelType; + labelType: Exclude; initialNavigationType: NavigationType; } @@ -119,16 +163,16 @@ const reducer = (state: State, action: ActionUnion): Stat }; } + if (action.type === ReducerActionType.SET_NEXT_FRAME) { + return { + ...state, + nextFrame: action.payload.nextFrame, + }; + } + return state; }; -function cancelCurrentCanvasOp(state: CombinedState): void { - const canvas = state.annotation.canvas.instance as Canvas; - if (canvas.mode() !== CanvasMode.IDLE) { - canvas.cancel(); - } -} - function SingleShapeSidebar(): JSX.Element { const appDispatch = useDispatch(); const store = useStore(); @@ -141,19 +185,24 @@ function SingleShapeSidebar(): JSX.Element { defaultLabel, defaultPointsCount, navigationType, + annotations, + activatedStateID, } = useSelector((_state: CombinedState) => ({ isCanvasReady: _state.annotation.canvas.ready, jobInstance: _state.annotation.job.instance as Job, frame: _state.annotation.player.frame.number, - keyMap: _state.shortcuts.keyMap, normalizedKeyMap: _state.shortcuts.normalizedKeyMap, + keyMap: _state.shortcuts.keyMap, defaultLabel: _state.annotation.job.queryParameters.defaultLabel, defaultPointsCount: _state.annotation.job.queryParameters.defaultPointsCount, navigationType: _state.annotation.player.navigationType, + annotations: _state.annotation.annotations.states, + activatedStateID: _state.annotation.annotations.activatedStateID, }), shallowEqual); const [state, dispatch] = useReducer(reducer, { autoNextFrame: true, + nextFrame: null, saveOnFinish: true, pointsCountIsPredefined: true, pointsCount: defaultPointsCount || 1, @@ -163,88 +212,105 @@ function SingleShapeSidebar(): JSX.Element { initialNavigationType: navigationType, }); + const unmountedRef = useRef(false); const savingRef = useRef(false); - const nextFrame = useCallback((): void => { - let promise = Promise.resolve(null); - if (frame < jobInstance.stopFrame) { - promise = jobInstance.annotations.search(frame + 1, jobInstance.stopFrame, { - allowDeletedFrames: false, - ...(navigationType === NavigationType.EMPTY ? { - generalFilters: { - isEmptyFrame: true, - }, - } : {}), - }); - } - - promise.then((foundFrame: number | null) => { - if (typeof foundFrame === 'number') { - appDispatch(changeFrameAsync(foundFrame)); - } else if (state.saveOnFinish && !savingRef.current) { - Modal.confirm({ - title: 'You finished the job', - content: 'Please, confirm further action', - cancelText: 'Stay on the page', - okText: 'Submit results', - className: 'cvat-single-shape-annotation-submit-job-modal', - onOk: () => { - function reset(): void { - savingRef.current = false; - } - - function showSubmittedInfo(): void { - Modal.info({ - closable: false, - title: 'Annotations submitted', - content: 'You may close the window', - className: 'cvat-single-shape-annotation-submit-success-modal', - }); - } - - savingRef.current = true; - if (jobInstance.annotations.hasUnsavedChanges()) { - appDispatch(saveAnnotationsAsync(() => { - jobInstance.state = JobState.COMPLETED; - jobInstance.save().then(showSubmittedInfo).finally(reset); - })).catch(reset); - } else { - jobInstance.state = JobState.COMPLETED; - jobInstance.save().then(showSubmittedInfo).finally(reset); - } - }, - }); - } - }); - }, [state.saveOnFinish, frame, jobInstance, navigationType]); - const canvasInitializerRef = useRef<() => void | null>(() => {}); canvasInitializerRef.current = (): void => { const canvas = store.getState().annotation.canvas.instance as Canvas; if (isCanvasReady && canvas.mode() !== CanvasMode.DRAW && state.label && state.labelType !== LabelType.ANY) { + // we remember active object type and active label + // to assign these values in default drawdone event listener appDispatch(rememberObject({ - activeLabelID: state.label.id, activeObjectType: ObjectType.SHAPE, + activeLabelID: state.label.id, })); canvas.draw({ enabled: true, + crosshair: true, shapeType: state.labelType, numberOfPoints: state.pointsCountIsPredefined ? state.pointsCount : undefined, - crosshair: true, }); } }; + const getNextFrame = useCallback(() => { + if (frame + 1 > jobInstance.stopFrame) { + return Promise.resolve(null); + } + + return jobInstance.annotations.search(frame + 1, jobInstance.stopFrame, { + allowDeletedFrames: false, + ...(navigationType === NavigationType.EMPTY ? { + generalFilters: { + isEmptyFrame: true, + }, + } : {}), + }) as Promise; + }, [jobInstance, navigationType, frame]); + + const finishOnThisFrame = useCallback((): void => { + if (typeof state.nextFrame === 'number') { + appDispatch(changeFrameAsync(state.nextFrame)); + } else if (state.saveOnFinish && !savingRef.current) { + savingRef.current = true; + if (jobInstance.annotations.hasUnsavedChanges()) { + appDispatch(saveAnnotationsAsync(() => { + jobInstance.state = JobState.COMPLETED; + jobInstance.save().then(showSubmittedInfo).finally(() => { + savingRef.current = false; + }); + })).catch(() => { + savingRef.current = false; + }); + } else { + jobInstance.state = JobState.COMPLETED; + jobInstance.save().then(showSubmittedInfo).finally(() => { + savingRef.current = false; + }); + } + } + }, [state.saveOnFinish, state.nextFrame, jobInstance]); + useEffect(() => { + const defaultLabelInstance = defaultLabel ? state.labels + .find((_label) => _label.name === defaultLabel) ?? null : null; + + const labelInstance = defaultLabelInstance ?? state.labels[0]; + if (labelInstance) { + dispatch(reducerActions.setActiveLabel(labelInstance, labelInstance.type as State['labelType'])); + } + + appDispatch(setNavigationType(NavigationType.EMPTY)); + cancelCurrentCanvasOp(store.getState()); + return () => { + unmountedRef.current = true; + appDispatch(setNavigationType(state.initialNavigationType)); + cancelCurrentCanvasOp(store.getState()); + }; + }, []); + + useEffect(() => { + getNextFrame().then((_frame: number | null) => { + dispatch({ + type: ReducerActionType.SET_NEXT_FRAME, + payload: { nextFrame: _frame }, + }); + }); + }, [getNextFrame]); + + useEffect(() => { + // when canvas finishes drawing object it first sends canvas.cancel then it sends canvas.drawn, + // we do not need onCancel effect to be applied if object is drawn so, we introduce applied flag + let drawDoneEffectApplied = false; + + const drawnObjects = annotations.filter((_state) => _state.objectType !== ObjectType.TAG); const canvas = store.getState().annotation.canvas.instance as Canvas; const onDrawDone = (): void => { - setTimeout(() => { - if (state.autoNextFrame) { - nextFrame(); - } else { - canvasInitializerRef.current(); - } - }, 100); + drawDoneEffectApplied = true; + if (!unmountedRef.current && state.autoNextFrame) { + setTimeout(finishOnThisFrame, 30); + } }; const onCancel = (): void => { @@ -253,46 +319,34 @@ function SingleShapeSidebar(): JSX.Element { // but there are some cases when only canvas.cancel is triggered (e.g. when drawn shape was not correct) // in this case need to re-run drawing process setTimeout(() => { - canvasInitializerRef.current(); - }); + if (!unmountedRef.current && !drawDoneEffectApplied && !drawnObjects.length) { + canvasInitializerRef.current(); + } + }, 50); }; (canvas as Canvas).html().addEventListener('canvas.drawn', onDrawDone); (canvas as Canvas).html().addEventListener('canvas.canceled', onCancel); - return (() => { - // should stay prior mount useEffect to remove event handlers before final cancel() is called + return (() => { (canvas as Canvas).html().removeEventListener('canvas.drawn', onDrawDone); (canvas as Canvas).html().removeEventListener('canvas.canceled', onCancel); }); - }, [nextFrame, state.autoNextFrame, state.saveOnFinish]); + }, [finishOnThisFrame, annotations, state.autoNextFrame, state.saveOnFinish]); useEffect(() => { - const labelInstance = (defaultLabel ? jobInstance.labels - .find((_label) => _label.name === defaultLabel) : state.labels[0] || null); - if (labelInstance) { - dispatch(reducerActions.setActiveLabel(labelInstance)); - } - - appDispatch(setNavigationType(NavigationType.EMPTY)); - cancelCurrentCanvasOp(store.getState()); - return () => { - appDispatch(setNavigationType(state.initialNavigationType)); + if (isCanvasReady) { + const drawnObjects = annotations.filter((_state) => _state.objectType !== ObjectType.TAG); cancelCurrentCanvasOp(store.getState()); - }; - }, []); - - useEffect(() => { - cancelCurrentCanvasOp(store.getState()); - canvasInitializerRef.current(); - }, [isCanvasReady, state.label, state.labelType, state.pointsCount, state.pointsCountIsPredefined]); - - let message = ''; - if (state.labelType === LabelType.POINTS) { - message = `${state.pointsCount === 1 ? 'one point' : `${state.pointsCount} points`}`; - } else { - message = `${state.labelType === LabelType.ELLIPSE ? 'an ellipse' : `a ${state.labelType}`}`; - } + if (!drawnObjects.length) { + canvasInitializerRef.current(); + } + } + }, [ + isCanvasReady, annotations, + state.label, state.labelType, + state.pointsCount, state.pointsCountIsPredefined, + ]); const siderProps: SiderProps = { className: 'cvat-single-shape-annotation-sidebar', @@ -306,6 +360,7 @@ function SingleShapeSidebar(): JSX.Element { const subKeyMap = { CANCEL: keyMap.CANCEL, + DELETE_OBJECT: keyMap.DELETE_OBJECT, SWITCH_DRAW_MODE: keyMap.SWITCH_DRAW_MODE, }; @@ -319,6 +374,13 @@ function SingleShapeSidebar(): JSX.Element { const canvas = store.getState().annotation.canvas.instance as Canvas; canvas.draw({ enabled: false }); }, + DELETE_OBJECT: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + const objectStateToRemove = annotations.find((_state) => _state.clientID === activatedStateID); + if (objectStateToRemove) { + appDispatch(removeObjectAsync(objectStateToRemove, event?.shiftKey || false)); + } + }, }; if (!state.labels.length) { @@ -344,26 +406,19 @@ function SingleShapeSidebar(): JSX.Element { - Annotate - {` ${(state.label as Label).name} `} - on the image, using - {` ${message} `} - - )} + message={makeMessage(state.label, state.labelType, state.pointsCount)} /> - + - + {typeof state.nextFrame === 'number' ? ( + + ) : ( + + )} + { typeof state.nextFrame === 'number' ? ( +
  • + + Click + {' Skip '} + if there is nothing to annotate + +
  • + ) : ( +
  • + + Click + {' Submit Results '} + to finish the job + +
  • + )}
  • - Click - {' Skip '} - if there is nothing to annotate -
  • -
  • - Hold - {' [Alt] '} - button to avoid drawing on click + + Hold + {' [Alt] '} + button to avoid drag the image and avoid drawing +
  • - Press - {` ${normalizedKeyMap.UNDO} `} - to undo a created object + + Press + {` ${normalizedKeyMap.UNDO} `} + to undo a created object +
  • { (!isPolylabel || !state.pointsCountIsPredefined || state.pointsCount > 1) && (
  • - Press - {` ${normalizedKeyMap.CANCEL} `} - to reset drawing process + + Press + {` ${normalizedKeyMap.CANCEL} `} + to reset drawing process +
  • ) } { (isPolylabel && (!state.pointsCountIsPredefined || state.pointsCount > 1)) && (
  • - Press - {` ${normalizedKeyMap.SWITCH_DRAW_MODE} `} - to finish drawing process + + Press + {` ${normalizedKeyMap.SWITCH_DRAW_MODE} `} + to finish drawing process +
  • ) } + { activatedStateID !== null && ( +
  • + + Press + {` ${normalizedKeyMap.DELETE_OBJECT} `} + to delete current object + +
  • + )} )} /> @@ -419,7 +503,7 @@ function SingleShapeSidebar(): JSX.Element { dispatch(reducerActions.setActiveLabel(label))} + onChange={(label) => dispatch(reducerActions.setActiveLabel(label, label.type as State['labelType']))} />
    @@ -436,7 +520,7 @@ function SingleShapeSidebar(): JSX.Element { - {labelsMappingVisible && ( + {isDetector && ( 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 d1086ca9a522..241f2338235b 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 @@ -24,7 +24,6 @@ interface StateToProps { task: any; detectors: MLModel[]; reid: MLModel[]; - classifiers: MLModel[]; } interface DispatchToProps { @@ -34,14 +33,13 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState): StateToProps { const { models } = state; - const { detectors, reid, classifiers } = models; + const { detectors, reid } = models; return { visible: models.modelRunnerIsVisible, task: models.modelRunnerTask, reid, detectors, - classifiers, }; } @@ -58,10 +56,10 @@ function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { function ModelRunnerDialog(props: StateToProps & DispatchToProps): JSX.Element { const { - reid, detectors, classifiers, task, visible, runInference, closeDialog, + reid, detectors, task, visible, runInference, closeDialog, } = props; - const models = [...reid, ...detectors, ...classifiers]; + const models = [...reid, ...detectors]; const [taskInstance, setTaskInstance] = useState(null); useEffect(() => { diff --git a/cvat-ui/src/components/models-page/deployed-model-item.tsx b/cvat-ui/src/components/models-page/deployed-model-item.tsx index 32acc8cd43c4..5aad954a3480 100644 --- a/cvat-ui/src/components/models-page/deployed-model-item.tsx +++ b/cvat-ui/src/components/models-page/deployed-model-item.tsx @@ -119,7 +119,7 @@ export default function DeployedModelItem(props: Props): JSX.Element { {model.provider} - {model.kind} + {model.displayKind} 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 ce1e70196040..a31751e20d33 100644 --- a/cvat-ui/src/components/models-page/deployed-models-list.tsx +++ b/cvat-ui/src/components/models-page/deployed-models-list.tsx @@ -34,14 +34,13 @@ export default function DeployedModelsListComponent(props: Props): JSX.Element { 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 totalCount = useSelector((state: CombinedState) => state.models.totalCount); const dispatch = useDispatch(); const { query } = props; const { page } = query; const models = setUpModelsList( - [...interactors, ...detectors, ...trackers, ...reid, ...classifiers], + [...interactors, ...detectors, ...trackers, ...reid], page, ); diff --git a/cvat-ui/src/components/resource-sorting-filtering/filtering.tsx b/cvat-ui/src/components/resource-sorting-filtering/filtering.tsx index f56d9377bbac..256e6d05aa8e 100644 --- a/cvat-ui/src/components/resource-sorting-filtering/filtering.tsx +++ b/cvat-ui/src/components/resource-sorting-filtering/filtering.tsx @@ -88,7 +88,7 @@ export default function ResourceFilterHOC( }; function isValidTree(tree: ImmutableTree): boolean { - return (QbUtils.queryString(tree, config) || '').trim().length > 0 && QbUtils.isValidTree(tree); + return (QbUtils.queryString(tree, config) || '').trim().length > 0 && QbUtils.isValidTree(tree, config); } function unite(filters: string[]): string { diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index b25803b16bc7..a847e873bbfd 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -1,10 +1,10 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import './styles.scss'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router'; import { useDispatch, useSelector } from 'react-redux'; import { Row, Col } from 'antd/lib/grid'; @@ -31,45 +31,39 @@ function TaskPageComponent(): JSX.Element { const [taskInstance, setTaskInstance] = useState(null); const [fetchingTask, setFetchingTask] = useState(true); const [updatingTask, setUpdatingTask] = useState(false); - const mounted = useRef(false); const deletes = useSelector((state: CombinedState) => state.tasks.activities.deletes); - const receieveTask = (): void => { + const receieveTask = (): Promise => { if (Number.isInteger(id)) { - core.tasks.get({ id }) - .then(([task]: Task[]) => { - if (task && mounted.current) { - setTaskInstance(task); - } - }).catch((error: Error) => { - if (mounted.current) { - notification.error({ - message: 'Could not receive the requested task from the server', - description: error.toString(), - }); - } - }).finally(() => { - if (mounted.current) { - setFetchingTask(false); - } + const promise = core.tasks.get({ id }); + promise.then(([task]: Task[]) => { + if (task) { + setTaskInstance(task); + } + }).catch((error: Error) => { + notification.error({ + message: 'Could not receive the requested task from the server', + description: error.toString(), }); - } else { - notification.error({ - message: 'Could not receive the requested task from the server', - description: `Requested task id "${id}" is not valid`, }); - setFetchingTask(false); + + return promise; } + + notification.error({ + message: 'Could not receive the requested task from the server', + description: `Requested task id "${id}" is not valid`, + }); + + return Promise.reject(); }; useEffect(() => { - receieveTask(); + receieveTask().finally(() => { + setFetchingTask(false); + }); dispatch(getInferenceStatusAsync()); - mounted.current = true; - return () => { - mounted.current = false; - }; }, []); useEffect(() => { @@ -97,9 +91,7 @@ function TaskPageComponent(): JSX.Element { new Promise((resolve, reject) => { setUpdatingTask(true); task.save().then((updatedTask: Task) => { - if (mounted.current) { - setTaskInstance(updatedTask); - } + setTaskInstance(updatedTask); resolve(); }).catch((error: Error) => { notification.error({ @@ -109,9 +101,7 @@ function TaskPageComponent(): JSX.Element { }); reject(); }).finally(() => { - if (mounted.current) { - setUpdatingTask(false); - } + setUpdatingTask(false); }); }) ); @@ -119,18 +109,15 @@ function TaskPageComponent(): JSX.Element { const onJobUpdate = (job: Job): void => { setUpdatingTask(true); job.save().then(() => { - if (mounted.current) { - receieveTask(); - } + receieveTask().finally(() => { + setUpdatingTask(false); + }); }).catch((error: Error) => { + setUpdatingTask(false); notification.error({ message: 'Could not update the job', description: error.toString(), }); - }).finally(() => { - if (mounted.current) { - setUpdatingTask(false); - } }); }; diff --git a/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx b/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx index 74ac036c6270..0e400a1adb8c 100644 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx +++ b/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx @@ -19,7 +19,7 @@ import { Canvas } from 'cvat-canvas-wrapper'; import { ObjectState, QualityConflict } from 'cvat-core-wrapper'; interface OwnProps { - readonly: boolean; + readonly?: boolean; } interface StateToProps { diff --git a/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx b/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx index 697382177faa..2e20c8e994b0 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx @@ -50,8 +50,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { showImportModal(jobInstance: Job): void { dispatch(importActions.openImportDatasetModal(jobInstance)); }, - removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean) { - dispatch(removeAnnotationsAsyncAction(startnumber, endnumber, delTrackKeyframesOnly)); + removeAnnotations(startFrame: number, stopFrame: number, delTrackKeyframesOnly: boolean) { + dispatch(removeAnnotationsAsyncAction(startFrame, stopFrame, delTrackKeyframesOnly)); }, setForceExitAnnotationFlag(forceExit: boolean): void { dispatch(setForceExitAnnotationFlagAction(forceExit)); diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 27a35d15bff5..7b610e5c1a76 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -412,7 +412,6 @@ export interface ModelsState { detectors: MLModel[]; trackers: MLModel[]; reid: MLModel[]; - classifiers: MLModel[]; totalCount: number; requestedInferenceIDs: { [index: string]: boolean; diff --git a/cvat-ui/src/reducers/models-reducer.ts b/cvat-ui/src/reducers/models-reducer.ts index 3c7fa6292bae..e1eb8158819b 100644 --- a/cvat-ui/src/reducers/models-reducer.ts +++ b/cvat-ui/src/reducers/models-reducer.ts @@ -18,7 +18,6 @@ const defaultState: ModelsState = { detectors: [], trackers: [], reid: [], - classifiers: [], modelRunnerIsVisible: false, modelRunnerTask: null, requestedInferenceIDs: {}, @@ -61,9 +60,6 @@ export default function (state = defaultState, action: ModelsActions | AuthActio reid: action.payload.models.filter((model: MLModel) => ( model.kind === ModelKind.REID )), - classifiers: action.payload.models.filter((model: MLModel) => ( - model.kind === ModelKind.CLASSIFIER - )), totalCount: action.payload.count, initialized: true, fetching: false, diff --git a/cvat/__init__.py b/cvat/__init__.py index e7c107bc0e52..8a7bcfafad16 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -4,6 +4,6 @@ from cvat.utils.version import get_version -VERSION = (2, 14, 3, 'final', 0) +VERSION = (2, 14, 4, 'final', 0) __version__ = get_version(VERSION) diff --git a/cvat/apps/engine/backup.py b/cvat/apps/engine/backup.py index 1a316a14cfc5..3ad2d1ff01d5 100644 --- a/cvat/apps/engine/backup.py +++ b/cvat/apps/engine/backup.py @@ -1,8 +1,9 @@ # Copyright (C) 2021-2022 Intel Corporation -# Copyright (C) 2022-2023 CVAT.ai Corporation +# Copyright (C) 2022-2024 CVAT.ai Corporation # # SPDX-License-Identifier: MIT +from logging import Logger import io import os from enum import Enum @@ -46,7 +47,7 @@ from cvat.apps.engine.cloud_provider import import_resource_from_cloud_storage, export_resource_to_cloud_storage from cvat.apps.engine.location import StorageType, get_location_configuration from cvat.apps.engine.view_utils import get_cloud_storage_for_import_or_export -from cvat.apps.dataset_manager.views import TASK_CACHE_TTL, PROJECT_CACHE_TTL, get_export_cache_dir, clear_export_cache, log_exception +from cvat.apps.dataset_manager.views import TASK_CACHE_TTL, PROJECT_CACHE_TTL, get_export_cache_dir, log_exception from cvat.apps.dataset_manager.bindings import CvatImportError slogger = ServerLogManager(__name__) @@ -904,7 +905,7 @@ def _create_backup(db_instance, Exporter, output_path, logger, cache_ttl): archive_ctime = os.path.getctime(output_path) scheduler = django_rq.get_scheduler(settings.CVAT_QUEUES.IMPORT_DATA.value) cleaning_job = scheduler.enqueue_in(time_delta=cache_ttl, - func=clear_export_cache, + func=_clear_export_cache, file_path=output_path, file_ctime=archive_ctime, logger=logger) @@ -1189,3 +1190,15 @@ def import_task(request, queue_name, filename=None): location_conf=location_conf, filename=filename ) + +def _clear_export_cache(file_path: str, file_ctime: float, logger: Logger) -> None: + try: + if os.path.exists(file_path) and os.path.getctime(file_path) == file_ctime: + os.remove(file_path) + + logger.info( + "Export cache file '{}' successfully removed" \ + .format(file_path)) + except Exception: + log_exception(logger) + raise diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index aa4b682283c7..d4a7d602f564 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -1,9 +1,10 @@ # Copyright (C) 2020-2022 Intel Corporation -# Copyright (C) 2023 CVAT.ai Corporation +# Copyright (C) 2023-2024 CVAT.ai Corporation # # SPDX-License-Identifier: MIT from contextlib import ExitStack +from datetime import timedelta import io from itertools import product import os @@ -24,6 +25,7 @@ import json import av +import django_rq import numpy as np from pdf2image import convert_from_bytes from pyunpack import Archive @@ -3032,6 +3034,33 @@ def test_api_v2_tasks_id_export_somebody(self): def test_api_v2_tasks_id_export_no_auth(self): self._run_api_v2_tasks_id_export_import(None) + def test_can_remove_export_cache_automatically_after_successful_export(self): + self._create_tasks() + task_id = self.tasks[0]["id"] + user = self.admin + + with mock.patch('cvat.apps.engine.backup.TASK_CACHE_TTL', new=timedelta(hours=10)): + response = self._run_api_v2_tasks_id_export(task_id, user) + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + + response = self._run_api_v2_tasks_id_export(task_id, user) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + scheduler = django_rq.get_scheduler(settings.CVAT_QUEUES.IMPORT_DATA.value) + scheduled_jobs = list(scheduler.get_jobs()) + cleanup_job = next( + j for j in scheduled_jobs if j.func_name.endswith('.engine.backup._clear_export_cache') + ) + + export_path = cleanup_job.kwargs['file_path'] + self.assertTrue(os.path.isfile(export_path)) + + from cvat.apps.engine.backup import _clear_export_cache + _clear_export_cache(**cleanup_job.kwargs) + + self.assertFalse(os.path.isfile(export_path)) + + def generate_random_image_file(filename): gen = random.SystemRandom() width = gen.randint(100, 800) diff --git a/cvat/apps/events/export.py b/cvat/apps/events/export.py index 0b97775e938c..da248db4d292 100644 --- a/cvat/apps/events/export.py +++ b/cvat/apps/events/export.py @@ -1,7 +1,8 @@ -# Copyright (C) 2023 CVAT.ai Corporation +# Copyright (C) 2023-2024 CVAT.ai Corporation # # SPDX-License-Identifier: MIT +from logging import Logger import os import csv from datetime import datetime, timedelta, timezone @@ -16,7 +17,7 @@ from rest_framework import serializers, status from rest_framework.response import Response -from cvat.apps.dataset_manager.views import clear_export_cache, log_exception +from cvat.apps.dataset_manager.views import log_exception from cvat.apps.engine.log import ServerLogManager from cvat.apps.engine.utils import sendfile @@ -72,7 +73,7 @@ def _create_csv(query_params, output_filename, cache_ttl): archive_ctime = os.path.getctime(output_filename) scheduler = django_rq.get_scheduler(settings.CVAT_QUEUES.EXPORT_DATA.value) cleaning_job = scheduler.enqueue_in(time_delta=cache_ttl, - func=clear_export_cache, + func=_clear_export_cache, file_path=output_filename, file_ctime=archive_ctime, logger=slogger.glob, @@ -168,3 +169,15 @@ def export(request, filter_query, queue_name): result_ttl=ttl, failure_ttl=ttl) return Response(data=response_data, status=status.HTTP_202_ACCEPTED) + +def _clear_export_cache(file_path: str, file_ctime: float, logger: Logger) -> None: + try: + if os.path.exists(file_path) and os.path.getctime(file_path) == file_ctime: + os.remove(file_path) + + logger.info( + "Export cache file '{}' successfully removed" \ + .format(file_path)) + except Exception: + log_exception(logger) + raise diff --git a/cvat/schema.yml b/cvat/schema.yml index 8a88d9e57b95..067c52568cd8 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.14.3 + version: 2.14.4 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: diff --git a/docker-compose.yml b/docker-compose.yml index fea51cd0a88b..8824a414ff8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,6 @@ # SPDX-License-Identifier: MIT x-backend-env: &backend-env - CLICKHOUSE_HOST: clickhouse CVAT_POSTGRES_HOST: cvat_db CVAT_REDIS_INMEM_HOST: cvat_redis_inmem CVAT_REDIS_INMEM_PORT: 6379 @@ -24,6 +23,13 @@ x-backend-deps: &backend-deps cvat_db: condition: service_started +x-clickhouse-env: &clickhouse-env + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_PORT: 8123 + CLICKHOUSE_DB: cvat + CLICKHOUSE_USER: user + CLICKHOUSE_PASSWORD: user + services: cvat_db: container_name: cvat_db @@ -72,7 +78,7 @@ services: cvat_server: container_name: cvat_server - image: cvat/server:${CVAT_VERSION:-v2.14.3} + image: cvat/server:${CVAT_VERSION:-v2.14.4} restart: always depends_on: <<: *backend-deps @@ -80,7 +86,7 @@ services: condition: service_started environment: - <<: *backend-env + <<: [*backend-env, *clickhouse-env] DJANGO_MODWSGI_EXTRA_ARGS: '' ALLOWED_HOSTS: '*' ADAPTIVE_AUTO_ANNOTATION: 'false' @@ -106,7 +112,7 @@ services: cvat_utils: container_name: cvat_utils - image: cvat/server:${CVAT_VERSION:-v2.14.3} + image: cvat/server:${CVAT_VERSION:-v2.14.4} restart: always depends_on: *backend-deps environment: @@ -123,7 +129,7 @@ services: cvat_worker_import: container_name: cvat_worker_import - image: cvat/server:${CVAT_VERSION:-v2.14.3} + image: cvat/server:${CVAT_VERSION:-v2.14.4} restart: always depends_on: *backend-deps environment: @@ -139,11 +145,11 @@ services: cvat_worker_export: container_name: cvat_worker_export - image: cvat/server:${CVAT_VERSION:-v2.14.3} + image: cvat/server:${CVAT_VERSION:-v2.14.4} restart: always depends_on: *backend-deps environment: - <<: *backend-env + <<: [*backend-env, *clickhouse-env] NUMPROCS: 2 command: run worker.export volumes: @@ -155,7 +161,7 @@ services: cvat_worker_annotation: container_name: cvat_worker_annotation - image: cvat/server:${CVAT_VERSION:-v2.14.3} + image: cvat/server:${CVAT_VERSION:-v2.14.4} restart: always depends_on: *backend-deps environment: @@ -171,7 +177,7 @@ services: cvat_worker_webhooks: container_name: cvat_worker_webhooks - image: cvat/server:${CVAT_VERSION:-v2.14.3} + image: cvat/server:${CVAT_VERSION:-v2.14.4} restart: always depends_on: *backend-deps environment: @@ -187,7 +193,7 @@ services: cvat_worker_quality_reports: container_name: cvat_worker_quality_reports - image: cvat/server:${CVAT_VERSION:-v2.14.3} + image: cvat/server:${CVAT_VERSION:-v2.14.4} restart: always depends_on: *backend-deps environment: @@ -203,11 +209,11 @@ services: cvat_worker_analytics_reports: container_name: cvat_worker_analytics_reports - image: cvat/server:${CVAT_VERSION:-v2.14.3} + image: cvat/server:${CVAT_VERSION:-v2.14.4} restart: always depends_on: *backend-deps environment: - <<: *backend-env + <<: [*backend-env, *clickhouse-env] NUMPROCS: 2 command: run worker.analytics_reports volumes: @@ -219,7 +225,7 @@ services: cvat_ui: container_name: cvat_ui - image: cvat/ui:${CVAT_VERSION:-v2.14.3} + image: cvat/ui:${CVAT_VERSION:-v2.14.4} restart: always depends_on: - cvat_server @@ -296,9 +302,7 @@ services: image: clickhouse/clickhouse-server:23.11-alpine restart: always environment: - - CLICKHOUSE_DB=cvat - - CLICKHOUSE_USER=user - - CLICKHOUSE_PASSWORD=user + <<: *clickhouse-env networks: cvat: aliases: @@ -314,10 +318,7 @@ services: depends_on: - cvat_clickhouse environment: - - CLICKHOUSE_DB=cvat - - CLICKHOUSE_USER=user - - CLICKHOUSE_PASSWORD=user - - CLICKHOUSE_HOST=clickhouse + <<: *clickhouse-env networks: cvat: aliases: @@ -329,15 +330,16 @@ services: image: grafana/grafana-oss:10.1.2 container_name: cvat_grafana environment: - - GF_PATHS_PROVISIONING=/etc/grafana/provisioning - - GF_AUTH_BASIC_ENABLED=false - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - - GF_AUTH_DISABLE_LOGIN_FORM=true - - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-clickhouse-datasource - - GF_SERVER_ROOT_URL=http://${CVAT_HOST:-localhost}/analytics - - GF_INSTALL_PLUGINS=https://github.com/grafana/clickhouse-datasource/releases/download/v2.0.7/grafana-clickhouse-datasource-2.0.7.linux_amd64.zip;grafana-clickhouse-datasource - - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/all_events.json + <<: *clickhouse-env + GF_PATHS_PROVISIONING: /etc/grafana/provisioning + GF_AUTH_BASIC_ENABLED: false + GF_AUTH_ANONYMOUS_ENABLED: true + GF_AUTH_ANONYMOUS_ORG_ROLE: Admin + GF_AUTH_DISABLE_LOGIN_FORM: true + GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-clickhouse-datasource + GF_SERVER_ROOT_URL: http://${CVAT_HOST:-localhost}/analytics + GF_INSTALL_PLUGINS: https://github.com/grafana/clickhouse-datasource/releases/download/v4.0.8/grafana-clickhouse-datasource-4.0.8.linux_amd64.zip;grafana-clickhouse-datasource + GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /var/lib/grafana/dashboards/all_events.json volumes: - ./components/analytics/grafana/dashboards/:/var/lib/grafana/dashboards/:ro entrypoint: @@ -345,24 +347,25 @@ services: - -euc - | mkdir -p /etc/grafana/provisioning/datasources - cat < /etc/grafana/provisioning/datasources/ds.yaml + cat << 'EOF' > /etc/grafana/provisioning/datasources/ds.yaml apiVersion: 1 datasources: - name: 'ClickHouse' type: 'grafana-clickhouse-datasource' isDefault: true jsonData: - defaultDatabase: cvat - port: 9000 - server: clickhouse - username: user + defaultDatabase: $${CLICKHOUSE_DB} + port: $${CLICKHOUSE_PORT} + server: $${CLICKHOUSE_HOST} + username: $${CLICKHOUSE_USER} tlsSkipVerify: false + protocol: http secureJsonData: - password: user + password: $${CLICKHOUSE_PASSWORD} editable: true EOF mkdir -p /etc/grafana/provisioning/dashboards - cat < /etc/grafana/provisioning/dashboards/dashboard.yaml + cat << EOF > /etc/grafana/provisioning/dashboards/dashboard.yaml apiVersion: 1 providers: - name: cvat-logs diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 5f0f28316fc9..9976f28722d1 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -115,7 +115,7 @@ cvat: additionalVolumeMounts: [] replicas: 1 image: cvat/server - tag: v2.14.3 + tag: v2.14.4 imagePullPolicy: Always permissionFix: enabled: true @@ -139,7 +139,7 @@ cvat: frontend: replicas: 1 image: cvat/ui - tag: v2.14.3 + tag: v2.14.4 imagePullPolicy: Always labels: {} # test: test diff --git a/site/content/en/docs/administration/advanced/cvat-architecture.md b/site/content/en/docs/administration/advanced/cvat-architecture.md new file mode 100644 index 000000000000..8cb9ac1c55e6 --- /dev/null +++ b/site/content/en/docs/administration/advanced/cvat-architecture.md @@ -0,0 +1,36 @@ +--- +title: 'CVAT Architecture' +linkTitle: 'CVAT Architecture' +weight: 1 +description: 'Description of CVAT architecture and components' +--- + +This guide is designed to provide a comprehensive overview of the architecture and components +of the CVAT and to illustrate how each component interacts within the system. + +![CVAT Architecture](/images/cvat-arch.png) + + + +| Domain | Component   | Functionality | Description | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Analytics** | Vector                                                                                                                                                                                                                       | Event processing                                                                                               | There are several components that process events (backend, frontend, web UI). All events are sent to a single point - Vector, where they are processed and then redirected to ClickHouse. For more information, see [Analytics](/docs/administration/advanced/analytics/). | +|                         | ClickHouse                                                                                                                                                                                                                   | Event database                                                                                                 | Stores events. For more information, see [Analytics](/docs/administration/advanced/analytics/).                                                                                                                                                                             | +|                         | Grafana                                                                                                                                                                                                                     | Dashboards                                                                                                     | Data based on the web interface. For more information, see [Analytics](/docs/administration/advanced/analytics/).                                                                                                                                                           | +| **Data storage NFS** | RVVX access mode storage is required in case of multi-node deployment. Available with different types of storages:

  • AWS: Amazon Elastic File System (EFS)
  • Azure: Azure Files with NFS
  • GCP: Filestore NFS | Contains data required for CVAT operations                                                                   | It is necessary to have the capability for multiple mounting (across several nodes) in RWX mode. For more information, see [K8 Deployment with Helm](/docs/administration/advanced/k8s_deployment_with_helm/)                                                               | +| **Data cache** | Apache kvrocks                                                                                                                                                                                                               | Used for data caching (queries and search). Suitable for environments that require frequent database queries. | [Apache Kvrocks](https://kvrocks.apache.org/)                                                                                                                                                                                                                               | +| **Job queue** | Redis                                                                                                                                                                                                                       | Queue manager                                                                                                 |                                                                                                                                                                                                                                                                             | +| **Database** | PostgreSQL                                                                                                                                                                                                                   | Database                                                                                                       | A database where data is stored in a structured form.                                                                                                                                                                                                                       | +| **CVAT.ai Components** | Ingress Controller (can be disabled)                                                                                                                                                                                         | Routing traffic.                                                                                               | [CVAT deployment on Kubernetes with Helm](/docs/administration/advanced/k8s_deployment_with_helm/)                                                                                                                                                                         | +|                         | Authorization                                                                                                                                                                                                               | [Authorization service based on Open Policy Agent.](/docs/manual/advanced/iam_user_roles/)                     | +| **Backend CVAT** | Backend                                                                                                                                                                                                                     | Main framework                                                                                                 | Main engine, uses Django + Django DRF.                                                                                                                                                                                                                                     | +| **Workers** | Import Worker                                                                                                                                                                                                               | Everything related to loading data - creating tasks, uploading annotations, etc.                               |                                                                                                                                                                                                                                                                             | +|                         | Export Worker                                                                                                                                                                                                               | Everything related to exporting data - exporting results, creating dumps, etc.                                 |                                                                                                                                                                                                                                                                             | +|                         | Annotation Worker                                                                                                                                                                                                           | Auto-annotation tasks.                                                                                         |                                                                                                                                                                                                                                                                             | +|                         | Utils Worker                                                                                                                                                                                                                 | Responsible for tracking various file changes and more.                                                       |                                                                                                                                                                                                                                                                             | +|                         | Analytics Report                                                                                                                                                                                                             | Reports and analytics that are displayed in the CVAT interface.                                               |                                                                                                                                                                                                                                                                             | +|                         | Quality Report                                                                                                                                                                                                               | Analysis and reports on data quality.                                                                         |                                                                                                                                                                                                                                                                             | +|                         | Webhook Worker                                                                                                                                                                                                               | Manages webhooks.                                                                                             |                                                                                                                                                                                                                                                                             | +| Auto annotation         | Auto Annotation Nucio                                                                                                                                                                                                       | Microservice application, used for auto annotation.                                                           | [How to enable auto annotation feature.](/docs/administration/advanced/k8s_deployment_with_helm/#optional-enable-auto-annotation-feature)                                                                                                                                   | + + diff --git a/site/content/en/docs/manual/advanced/single-shape.md b/site/content/en/docs/manual/advanced/single-shape.md index 29f9a781f0cb..e6d02c994976 100644 --- a/site/content/en/docs/manual/advanced/single-shape.md +++ b/site/content/en/docs/manual/advanced/single-shape.md @@ -48,7 +48,7 @@ The **Single Shape** annotation mode has the following fields: | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Prompt for Shape and Label** | Displays the selected shape and label for the annotation task, for example: "Annotate **cat** on the image using **rectangle**". | | **Skip Button** | Enables moving to the next frame without annotating the current one, particularly useful when the frame does not have anything to be annotated. | -| **List of Hints** | Offers guidance on using the interface effectively, including:
    - Click **Skip** for frames without required annotations.
    - Hold the **Alt** button to avoid unintentional drawing (e.g. when you want only move the image).
    - Use the **Ctrl+Z** combination to undo the last annotation if needed.
    - Use the **Esc** button to completely reset the current drawing progress. | +| **List of Hints** | Offers guidance on using the interface effectively, including:
    - Click **Skip** for frames without required annotations.
    - Hold the **Alt** button to avoid unintentional drawing (e.g. when you want only move the image).
    - Use the **Ctrl+Z** combination to undo the last action if needed.
    - Use the **Esc** button to completely reset the current drawing progress. | | **Label selector** | Allows for the selection of different labels (`cat`, or `dog` in our example) for annotation within the interface. | | **Label type selector** | A drop-down list to select type of the label (rectangle, ellipce, etc). Only visible when the type of the shape is **Any**. | | **Options to Enable or Disable** | Provides configurable options to streamline the annotation process, such as:
    - **Automatically go to the next frame**.
    - **Automatically save when finish**.
    - **Navigate only empty frames**.
    - **Predefined number of points** - Specific to polyshape annotations, enabling this option auto-completes a shape once a predefined number of points is reached. Otherwise, pressing **N** is required to finalize the shape. | diff --git a/site/content/en/images/cvat-arch.png b/site/content/en/images/cvat-arch.png new file mode 100644 index 000000000000..8dcdbef87a90 Binary files /dev/null and b/site/content/en/images/cvat-arch.png differ diff --git a/site/package-lock.json b/site/package-lock.json index 192d1a1dd51b..df93ce839923 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -407,12 +407,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -947,9 +947,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -2829,12 +2829,12 @@ "requires": {} }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -3245,9 +3245,9 @@ "dev": true }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" diff --git a/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js b/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js index 3f6e4a4f2c60..057d1e548317 100644 --- a/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js +++ b/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js @@ -139,7 +139,7 @@ context('Object make a copy.', () => { const coordY = 400; for (let id = 1; id < countObject; id++) { // Point doesn't have a context menu - cy.get(`#cvat_canvas_shape_${id}`).trigger('mousemove', 'right'); + cy.get(`#cvat-objects-sidebar-state-item-${id}`).trigger('mouseover'); cy.get(`#cvat_canvas_shape_${id}`).should('have.class', 'cvat_canvas_shape_activated'); cy.get(`#cvat_canvas_shape_${id}`).rightclick({ force: true }); cy.get('.cvat-canvas-context-menu').should('be.visible'); diff --git a/tests/cypress/e2e/actions_projects_models/case_117_backup_restore_project_to_various_storages.js b/tests/cypress/e2e/actions_projects_models/case_117_backup_restore_project_to_various_storages.js index 8581fb4db70a..78dd48ad79d9 100644 --- a/tests/cypress/e2e/actions_projects_models/case_117_backup_restore_project_to_various_storages.js +++ b/tests/cypress/e2e/actions_projects_models/case_117_backup_restore_project_to_various_storages.js @@ -25,13 +25,11 @@ context('Tests source & target storage for backups.', () => { const imagesFolder = `cypress/fixtures/${imageFileName}`; const directoryToArchive = imagesFolder; - const serverHost = Cypress.config('baseUrl').includes('3000') ? 'localhost' : 'minio'; - const cloudStorageData = { displayName: 'Demo bucket', resource: 'public', manifest: 'manifest.jsonl', - endpointUrl: `http://${serverHost}:9000`, + endpointUrl: Cypress.config('minioUrl'), }; const storageConnectedToCloud = { diff --git a/tests/cypress/e2e/actions_tasks3/case_113_use_default_project_storage_for_import_export_annotations.js b/tests/cypress/e2e/actions_tasks3/case_113_use_default_project_storage_for_import_export_annotations.js index 96aa22a2a281..15ec3e1c3d16 100644 --- a/tests/cypress/e2e/actions_tasks3/case_113_use_default_project_storage_for_import_export_annotations.js +++ b/tests/cypress/e2e/actions_tasks3/case_113_use_default_project_storage_for_import_export_annotations.js @@ -36,13 +36,11 @@ context('Tests for source and target storage.', () => { secondY: 450, }; - const serverHost = Cypress.config('baseUrl').includes('3000') ? 'localhost' : 'minio'; - const cloudStorageData = { displayName: 'Demo bucket', resource: 'public', manifest: 'manifest.jsonl', - endpointUrl: `http://${serverHost}:9000`, + endpointUrl: Cypress.config('minioUrl'), }; const storageConnectedToCloud = { diff --git a/tests/cypress/e2e/actions_tasks3/case_114_use_default_task_storage_for_import_export_annotations.js b/tests/cypress/e2e/actions_tasks3/case_114_use_default_task_storage_for_import_export_annotations.js index 9c2dde278c6a..048f4916a8cb 100644 --- a/tests/cypress/e2e/actions_tasks3/case_114_use_default_task_storage_for_import_export_annotations.js +++ b/tests/cypress/e2e/actions_tasks3/case_114_use_default_task_storage_for_import_export_annotations.js @@ -37,13 +37,11 @@ context('Tests for source and target storage.', () => { secondY: 450, }; - const serverHost = Cypress.config('baseUrl').includes('3000') ? 'localhost' : 'minio'; - const cloudStorageData = { displayName: 'Demo bucket', resource: 'public', manifest: 'manifest.jsonl', - endpointUrl: `http://${serverHost}:9000`, + endpointUrl: Cypress.config('minioUrl'), }; const storageConnectedToCloud = { diff --git a/tests/cypress/e2e/actions_tasks3/case_115_use_custom_storage_for_import_export_annotations.js b/tests/cypress/e2e/actions_tasks3/case_115_use_custom_storage_for_import_export_annotations.js index 353e4f27526e..ac549171b63d 100644 --- a/tests/cypress/e2e/actions_tasks3/case_115_use_custom_storage_for_import_export_annotations.js +++ b/tests/cypress/e2e/actions_tasks3/case_115_use_custom_storage_for_import_export_annotations.js @@ -34,13 +34,11 @@ context('Import and export annotations: specify source and target storage in mod secondY: 450, }; - const serverHost = Cypress.config('baseUrl').includes('3000') ? 'localhost' : 'minio'; - const cloudStorageData = { displayName: 'Demo bucket', resource: 'public', manifest: 'manifest.jsonl', - endpointUrl: `http://${serverHost}:9000`, + endpointUrl: Cypress.config('minioUrl'), }; const project = { diff --git a/tests/cypress/e2e/actions_users/registration_involved/case_4_assign_task_job_users.js b/tests/cypress/e2e/actions_users/registration_involved/case_4_assign_task_job_users.js index e3d341938025..20a3bd81bfd5 100644 --- a/tests/cypress/e2e/actions_users/registration_involved/case_4_assign_task_job_users.js +++ b/tests/cypress/e2e/actions_users/registration_involved/case_4_assign_task_job_users.js @@ -133,7 +133,7 @@ context('Multiple users. Assign task, job. Deactivating users.', () => { cy.logout(); }); - it('Third user login. The task not exist. Logout', () => { + it('Third user login. The task does not exist. Logout', () => { cy.login(thirdUserName, thirdUser.password); cy.contains('strong', taskName).should('not.exist'); cy.logout(); @@ -146,7 +146,7 @@ context('Multiple users. Assign task, job. Deactivating users.', () => { cy.logout(); }); - it('The third user can open a job by a direct link.', () => { + it('The third user can not open the task, but can open the job by a direct link.', () => { cy.login(thirdUserName, thirdUser.password); cy.get('.cvat-item-task-name').should('not.exist'); cy.visit(`/tasks/${taskID}/jobs/${jobID}`); diff --git a/tests/cypress/e2e/features/single_object_annotation.js b/tests/cypress/e2e/features/single_object_annotation.js index fd8f76bb6df5..2b1ad294e1bd 100644 --- a/tests/cypress/e2e/features/single_object_annotation.js +++ b/tests/cypress/e2e/features/single_object_annotation.js @@ -4,6 +4,13 @@ /// +/* + TODO: Add new test cases + - After drawing with disabled "autoNext", user should be able to activate and drag/resize object + - User also should be able to remove activated object with shortcut + - After removing an object, drawing should start automatically +*/ + context('Single object annotation mode', { scrollBehavior: false }, () => { const taskName = 'Single object annotation mode'; const serverFiles = ['images/image_1.jpg', 'images/image_2.jpg', 'images/image_3.jpg']; @@ -60,17 +67,6 @@ context('Single object annotation mode', { scrollBehavior: false }, () => { }); } - function submitJob() { - cy.get('.cvat-single-shape-annotation-submit-job-modal').should('exist'); - cy.get('.cvat-single-shape-annotation-submit-job-modal').within(() => { cy.contains('Submit').click(); }); - - cy.intercept('PATCH', '/api/jobs/**').as('submitJob'); - cy.wait('@submitJob').its('response.statusCode').should('equal', 200); - - cy.get('.cvat-single-shape-annotation-submit-success-modal').should('exist'); - cy.get('.cvat-single-shape-annotation-submit-success-modal').within(() => { cy.contains('OK').click(); }); - } - function checkSingleShapeModeOpened() { cy.get('.cvat-workspace-selector').should('have.text', 'Single shape'); cy.get('.cvat-canvas-controls-sidebar').should('not.exist'); @@ -94,11 +90,16 @@ context('Single object annotation mode', { scrollBehavior: false }, () => { function drawObject(creatorFunction) { checkSingleShapeModeOpened(); + cy.intercept('PATCH', `/api/jobs/${jobID}/**`).as('submitJob'); for (let frame = 0; frame < frameCount; frame++) { checkFrameNum(frame); creatorFunction(); } - submitJob(); + + cy.wait('@submitJob').its('response.statusCode').should('equal', 200); + + cy.get('.cvat-single-shape-annotation-submit-success-modal').should('exist'); + cy.get('.cvat-single-shape-annotation-submit-success-modal').within(() => { cy.contains('OK').click(); }); } function changeLabel(labelName) { @@ -207,7 +208,7 @@ context('Single object annotation mode', { scrollBehavior: false }, () => { checkSingleShapeModeOpened(); // Skip - cy.get('.cvat-single-shape-annotation-sidebar-skip-wrapper').within(() => { + cy.get('.cvat-single-shape-annotation-sidebar-finish-frame-wrapper').within(() => { cy.contains('Skip').click(); }); checkFrameNum(1); @@ -225,7 +226,7 @@ context('Single object annotation mode', { scrollBehavior: false }, () => { cy.get('[type="checkbox"]').uncheck(); }); clickPoints(polygonShape); - cy.get('.cvat-single-shape-annotation-submit-job-modal').should('not.exist'); + cy.get('.cvat-single-shape-annotation-submit-success-modal').should('not.exist'); // Navigate only on empty frames cy.get('.cvat-player-previous-button-empty').click(); diff --git a/tests/cypress/e2e/issues_prs2/issue_7428_importing_annotation_from_cloud_after_local_import.js b/tests/cypress/e2e/issues_prs2/issue_7428_importing_annotation_from_cloud_after_local_import.js index c31c06410c55..297f6a7f1cac 100644 --- a/tests/cypress/e2e/issues_prs2/issue_7428_importing_annotation_from_cloud_after_local_import.js +++ b/tests/cypress/e2e/issues_prs2/issue_7428_importing_annotation_from_cloud_after_local_import.js @@ -22,13 +22,11 @@ context('Incorrect cloud storage filename used in subsequent import.', () => { secondY: 200, }; - const serverHost = Cypress.config('baseUrl').includes('3000') ? 'localhost' : 'minio'; - const cloudStorageData = { displayName: 'Demo bucket', resource: 'public', manifest: 'manifest.jsonl', - endpointUrl: `http://${serverHost}:9000`, + endpointUrl: Cypress.config('minioUrl'), }; function uploadToTask({ diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 7d032bfe6706..316a196f23cc 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -269,7 +269,7 @@ Cypress.Commands.add('headlessLogin', (username = Cypress.env('user'), password Cypress.Commands.add('headlessCreateObject', (objects, jobID) => { cy.window().then(async ($win) => { const job = (await $win.cvat.jobs.get({ jobID }))[0]; - await job.annotations.clear(true); + await job.annotations.clear({ reload: true }); const objectStates = objects .map((object) => new $win.cvat.classes diff --git a/tests/cypress/support/commands_review_pipeline.js b/tests/cypress/support/commands_review_pipeline.js index b6c504a98bba..371330c2470f 100644 --- a/tests/cypress/support/commands_review_pipeline.js +++ b/tests/cypress/support/commands_review_pipeline.js @@ -8,25 +8,27 @@ Cypress.Commands.add('assignTaskToUser', (user) => { cy.get('.cvat-task-details-user-block').within(() => { if (user !== '') { + cy.intercept('GET', `/api/users?**search=${user}**`).as('searchUsers'); cy.get('.cvat-user-search-field').find('input').type(`${user}{Enter}`); + cy.wait('@searchUsers').its('response.statusCode').should('equal', 200); } else { cy.get('.cvat-user-search-field').find('input').clear(); cy.get('.cvat-user-search-field').find('input').type('{Enter}'); } - cy.get('.cvat-spinner').should('not.exist'); }); + + cy.get('.cvat-spinner').should('not.exist'); }); Cypress.Commands.add('assignJobToUser', (jobID, user) => { - cy.get('.cvat-jobs-list') - .contains('a', `Job #${jobID}`).parents('.cvat-job-item') - .find('.cvat-job-assignee-selector input').click(); - cy.get('.cvat-jobs-list') - .contains('a', `Job #${jobID}`).parents('.cvat-job-item') - .find('.cvat-job-assignee-selector input').clear(); + cy.get(`.cvat-job-item[data-row-id="${jobID}"]`).find('.cvat-job-assignee-selector input').click(); + cy.get(`.cvat-job-item[data-row-id="${jobID}"]`).find('.cvat-job-assignee-selector input').clear(); cy.intercept('PATCH', `/api/jobs/${jobID}`).as('patchJobAssignee'); if (user) { + cy.intercept('GET', `/api/users?**search=${user}**`).as('searchUsers'); + cy.get(`.cvat-job-item[data-row-id="${jobID}"]`).find('.cvat-job-assignee-selector input').type(user); + cy.wait('@searchUsers').its('response.statusCode').should('equal', 200); cy.get('.ant-select-dropdown') .should('be.visible') .not('.ant-select-dropdown-hidden') diff --git a/yarn.lock b/yarn.lock index 0d9b1ad1b0f1..1df7947f3948 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1134,13 +1134,20 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" + integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3", "@babel/template@^7.4.4": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" @@ -1984,18 +1991,6 @@ classnames "^2.3.2" rc-util "^5.24.4" -"@rc-component/trigger@^1.5.0": - version "1.18.3" - resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-1.18.3.tgz#b323b9e33f2700ca8d24a96f21401ab7b0eafdcd" - integrity sha512-Ksr25pXreYe1gX6ayZ1jLrOrl9OAUHUqnuhEx6MeHnNa1zVM5Y2Aj3Q35UrER0ns8D2cJYtmJtVli+i+4eKrvA== - dependencies: - "@babel/runtime" "^7.23.2" - "@rc-component/portal" "^1.1.0" - classnames "^2.3.2" - rc-motion "^2.0.0" - rc-resize-observer "^1.3.1" - rc-util "^5.38.0" - "@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.1.1.tgz#47973f1156ba63810c913eb46cbaedeba913874b" @@ -2008,40 +2003,42 @@ rc-resize-observer "^1.3.1" rc-util "^5.38.0" -"@react-awesome-query-builder/antd@^6.2.1": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@react-awesome-query-builder/antd/-/antd-6.4.2.tgz#639540c34cd17355de89a4fb16e6291673e5ba40" - integrity sha512-eG1izM1YMR1sY8xOn0pPGlHOIb4kw3mHnI8opfpfxex/B3k++Yrar+lu6zKcEKZSFMkzO82uL3ZiovIZys2vlg== +"@react-awesome-query-builder/antd@^6.5.2": + version "6.5.2" + resolved "https://registry.yarnpkg.com/@react-awesome-query-builder/antd/-/antd-6.5.2.tgz#f1e96beaa7aa6531ec2a5a2193c83e06928f0243" + integrity sha512-mQ7fhVF7hYOAFNzr6acSMf+vJF3CpWrnJ3UGoMXtsHbsC5+ILDOUoE7qnW7zYIoWLFk7zdURpAJpA+5ufwSY1A== dependencies: - "@react-awesome-query-builder/ui" "^6.4.2" + "@react-awesome-query-builder/ui" "^6.5.2" lodash "^4.17.21" - prop-types "^15.7.2" - rc-picker "^3.1.4" + prop-types "^15.8.1" + rc-picker "^4.5.0" -"@react-awesome-query-builder/core@^6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@react-awesome-query-builder/core/-/core-6.4.2.tgz#0f953964beeb8c7a0143fc91ec504dda236935f2" - integrity sha512-QPPgvYwY6mjmHVsvb05hPIRx3jxySLayfCI++++Rl8bTvbwOHz6yXn6KgipgvGBLzeiiT6NZ7glknL5/uYV73w== +"@react-awesome-query-builder/core@^6.5.2": + version "6.5.2" + resolved "https://registry.yarnpkg.com/@react-awesome-query-builder/core/-/core-6.5.2.tgz#004f9ddca017a83343f88e7673cebabeba01a22f" + integrity sha512-Ijpo/uO6upsclgIRLI8YLf+coFWkfjAS/WRB7+qeXmMX9MsaqKlYa8tk9dzYtGKK3msr6bdwCiMPlsd9CNDhrg== dependencies: + "@babel/runtime" "^7.24.5" clone "^2.1.2" - immutable "^3.8.2" + i18next "^23.11.5" + immutable "^4.3.6" json-logic-js "^2.0.2" lodash "^4.17.21" - moment "^2.29.4" + moment "^2.30.1" spel2js "^0.2.8" sqlstring "^2.3.3" -"@react-awesome-query-builder/ui@^6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@react-awesome-query-builder/ui/-/ui-6.4.2.tgz#2e24d8eb5b4826359de240fba1b742a4ecd24f77" - integrity sha512-rjK/5EKQikMVPX9I5T6Pr+7sKetiKTKvTB15TTyOR+6P9SxVTAQ/wt46zBO/yw5Tu4XcvpFxGdbV0RDy5yw+gQ== +"@react-awesome-query-builder/ui@^6.5.2": + version "6.5.2" + resolved "https://registry.yarnpkg.com/@react-awesome-query-builder/ui/-/ui-6.5.2.tgz#3c19e1de5b686786aa454037100f0d8f5e7cc370" + integrity sha512-onex0K8Ji/nZC+LrR2z7WwuUvo/selzb9aphFBRgGbzrdjkavag68h6sf1tMdaNv1Fki2fuvQcl8q4glCkVOeg== dependencies: - "@react-awesome-query-builder/core" "^6.4.2" - classnames "^2.3.1" + "@react-awesome-query-builder/core" "^6.5.2" + classnames "^2.5.1" lodash "^4.17.21" - prop-types "^15.7.2" - react-redux "^7.2.2" - redux "^4.2.0" + prop-types "^15.8.1" + react-redux "^8.1.3" + redux "^4.2.1" "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -2427,7 +2424,7 @@ dependencies: "@types/react" "*" -"@types/react-redux@^7.1.18", "@types/react-redux@^7.1.20": +"@types/react-redux@^7.1.18": version "7.1.33" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.33.tgz#53c5564f03f1ded90904e3c90f77e4bd4dc20b15" integrity sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg== @@ -3628,11 +3625,11 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browser-process-hrtime@^1.0.0: version "1.0.0" @@ -4421,7 +4418,7 @@ custom-error-instance@2.1.1: three "^0.156.1" "cvat-canvas@link:./cvat-canvas": - version "2.20.1" + version "2.20.3" dependencies: "@types/polylabel" "^1.0.5" polylabel "^1.1.0" @@ -4432,7 +4429,7 @@ custom-error-instance@2.1.1: svg.select.js "3.0.1" "cvat-core@link:./cvat-core": - version "15.0.5" + version "15.0.6" dependencies: axios "^1.6.0" axios-retry "^4.0.0" @@ -4448,7 +4445,7 @@ custom-error-instance@2.1.1: tus-js-client "^3.0.1" "cvat-data@link:./cvat-data": - version "2.0.0" + version "2.1.0" dependencies: async-mutex "^0.4.0" jszip "3.10.1" @@ -5567,10 +5564,10 @@ file-entry-cache@^7.0.0: dependencies: flat-cache "^3.2.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -6350,6 +6347,13 @@ husky@^6.0.0: resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== +i18next@^23.11.5: + version "23.11.5" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.11.5.tgz#d71eb717a7e65498d87d0594f2664237f9e361ef" + integrity sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -6384,16 +6388,16 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -immutable@^3.8.2: - version "3.8.2" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" - integrity sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg== - immutable@^4.0.0: version "4.3.5" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.5.tgz#f8b436e66d59f99760dc577f5c99a4fd2a5cc5a0" integrity sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw== +immutable@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" + integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== + import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -8757,7 +8761,7 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment@^2.29.2, moment@^2.29.4: +moment@^2.29.2, moment@^2.30.1: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== @@ -10228,15 +10232,17 @@ rc-pagination@~4.0.4: classnames "^2.3.2" rc-util "^5.38.0" -rc-picker@^3.1.4: - version "3.14.7" - resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-3.14.7.tgz#112f270ee933a1be3a59b32af1ea96c139bb9bac" - integrity sha512-+craFcClAOwu4R7lSlaiTAZRY4cWPgtE0+yji9stQkQR28C7WGTrZcyiq5AD7xfhXNV+82QmoJ8Aqg3duDYF6A== +rc-picker@^4.5.0: + version "4.6.5" + resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-4.6.5.tgz#f9155c8eb3fef6a08157541199ead5fb6b54409b" + integrity sha512-kxei2AgsK+kimg+pO12dDeBk3q31cZxGpkzo+O2pv2dXpPze8AI76fyR2PsilrTx8EvIwjZ66q3azkZOrjib8A== dependencies: - "@babel/runtime" "^7.10.1" - "@rc-component/trigger" "^1.5.0" + "@babel/runtime" "^7.24.7" + "@rc-component/trigger" "^2.0.0" classnames "^2.2.1" - rc-util "^5.30.0" + rc-overflow "^1.3.2" + rc-resize-observer "^1.4.0" + rc-util "^5.43.0" rc-picker@~4.5.0: version "4.5.0" @@ -10436,6 +10442,14 @@ rc-util@^5.0.1, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.2. "@babel/runtime" "^7.18.3" react-is "^18.2.0" +rc-util@^5.43.0: + version "5.43.0" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.43.0.tgz#bba91fbef2c3e30ea2c236893746f3e9b05ecc4c" + integrity sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw== + dependencies: + "@babel/runtime" "^7.18.3" + react-is "^18.2.0" + rc-virtual-list@^3.11.1, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2: version "3.11.5" resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.11.5.tgz#d4ba3bbd8e7ceae846f575a7d982d061ace1e76e" @@ -10506,11 +10520,6 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react- resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - react-is@^18.0.0, react-is@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" @@ -10547,19 +10556,7 @@ react-moment@^1.1.1: resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-1.1.3.tgz#829b21dfb279aa6db47ce4f1ac2555af17a1bcdc" integrity sha512-8EPvlUL8u6EknPp1ISF5MQ3wx2OHJVXIP/iZc4wRh3iV3XozftZERDv9ANZeAtMlhNNQHdFoqcZHFUkBSTONfA== -react-redux@^7.2.2: - version "7.2.9" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" - integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== - dependencies: - "@babel/runtime" "^7.15.4" - "@types/react-redux" "^7.1.20" - hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^17.0.2" - -react-redux@^8.0.2: +react-redux@^8.0.2, react-redux@^8.1.3: version "8.1.3" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== @@ -10731,7 +10728,7 @@ redux-thunk@^2.3.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@^4.0.0, redux@^4.1.1, redux@^4.2.0: +redux@^4.0.0, redux@^4.1.1, redux@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -13809,9 +13806,9 @@ write-file-atomic@^5.0.1: signal-exit "^4.0.1" ws@^8.11.0, ws@^8.13.0, ws@^8.2.3: - version "8.17.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" - integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^4.0.0: version "4.0.0"