diff --git a/CHANGELOG.md b/CHANGELOG.md index f70a88df..7268f099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,9 @@ Skeleton (), () - Support for Oracle OCI Buckets () - `cvat-sdk` and `cvat-cli` packages on PyPI () +- UI part for source and target storages () +- Backup import/export modals () +- Annotations import modal () ### Changed - Bumped nuclio version to 1.8.14 diff --git a/cvat-core/package.json b/cvat-core/package.json index 76d656d8..fdc299d9 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "6.0.2", + "version": "7.0.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/annotations-saver.ts b/cvat-core/src/annotations-saver.ts index fdc157aa..41800d6a 100644 --- a/cvat-core/src/annotations-saver.ts +++ b/cvat-core/src/annotations-saver.ts @@ -4,7 +4,7 @@ // SPDX-License-Identifier: MIT (() => { - const serverProxy = require('./server-proxy'); + const serverProxy = require('./server-proxy').default; const { Task } = require('./session'); const { ScriptingError } = require('./exceptions'); diff --git a/cvat-core/src/annotations.ts b/cvat-core/src/annotations.ts index 3b6d2836..3b1fdbd3 100644 --- a/cvat-core/src/annotations.ts +++ b/cvat-core/src/annotations.ts @@ -1,404 +1,387 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -(() => { - const serverProxy = require('./server-proxy'); - const Collection = require('./annotations-collection'); - const AnnotationsSaver = require('./annotations-saver'); - const AnnotationsHistory = require('./annotations-history').default; - const { checkObjectType } = require('./common'); - const { Project } = require('./project'); - const { Task, Job } = require('./session'); - const { Loader } = require('./annotation-formats'); - const { ScriptingError, DataError, ArgumentError } = require('./exceptions'); - const { getDeletedFrames } = require('./frames'); - - const jobCache = new WeakMap(); - const taskCache = new WeakMap(); - - function getCache(sessionType) { - if (sessionType === 'task') { - return taskCache; - } +import { Storage } from './storage'; +const serverProxy = require('./server-proxy').default; +const Collection = require('./annotations-collection'); +const AnnotationsSaver = require('./annotations-saver'); +const AnnotationsHistory = require('./annotations-history').default; +const { checkObjectType } = require('./common'); +const Project = require('./project').default; +const { Task, Job } = require('./session'); +const { ScriptingError, DataError, ArgumentError } = require('./exceptions'); +const { getDeletedFrames } = require('./frames'); + +const jobCache = new WeakMap(); +const taskCache = new WeakMap(); + +function getCache(sessionType) { + if (sessionType === 'task') { + return taskCache; + } - if (sessionType === 'job') { - return jobCache; - } + if (sessionType === 'job') { + return jobCache; + } - throw new ScriptingError(`Unknown session type was received ${sessionType}`); - } - - async function getAnnotationsFromServer(session) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); - - if (!cache.has(session)) { - const rawAnnotations = await serverProxy.annotations.getAnnotations(sessionType, session.id); - - // Get meta information about frames - const startFrame = sessionType === 'job' ? session.startFrame : 0; - const stopFrame = sessionType === 'job' ? session.stopFrame : session.size - 1; - const frameMeta = {}; - for (let i = startFrame; i <= stopFrame; i++) { - frameMeta[i] = await session.frames.get(i); - } - frameMeta.deleted_frames = await getDeletedFrames(sessionType, session.id); - - const history = new AnnotationsHistory(); - const collection = new Collection({ - labels: session.labels || session.task.labels, - history, - startFrame, - stopFrame, - frameMeta, - }); - - // eslint-disable-next-line no-unsanitized/method - collection.import(rawAnnotations); - const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); - cache.set(session, { collection, saver, history }); - } + throw new ScriptingError(`Unknown session type was received ${sessionType}`); +} + +async function getAnnotationsFromServer(session) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); + + if (!cache.has(session)) { + const rawAnnotations = await serverProxy.annotations.getAnnotations(sessionType, session.id); + + // Get meta information about frames + const startFrame = sessionType === 'job' ? session.startFrame : 0; + const stopFrame = sessionType === 'job' ? session.stopFrame : session.size - 1; + const frameMeta = {}; + for (let i = startFrame; i <= stopFrame; i++) { + frameMeta[i] = await session.frames.get(i); + } + frameMeta.deleted_frames = await getDeletedFrames(sessionType, session.id); + + const history = new AnnotationsHistory(); + const collection = new Collection({ + labels: session.labels || session.task.labels, + history, + startFrame, + stopFrame, + frameMeta, + }); + + // eslint-disable-next-line no-unsanitized/method + collection.import(rawAnnotations); + const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); + cache.set(session, { collection, saver, history }); } +} - async function closeSession(session) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); +export async function closeSession(session) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - if (cache.has(session)) { - cache.delete(session); - } + if (cache.has(session)) { + cache.delete(session); } +} - async function getAnnotations(session, frame, allTracks, filters) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); +export async function getAnnotations(session, frame, allTracks, filters) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - if (cache.has(session)) { - return cache.get(session).collection.get(frame, allTracks, filters); - } - - await getAnnotationsFromServer(session); + if (cache.has(session)) { return cache.get(session).collection.get(frame, allTracks, filters); } - async function saveAnnotations(session, onUpdate) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + await getAnnotationsFromServer(session); + return cache.get(session).collection.get(frame, allTracks, filters); +} - if (cache.has(session)) { - await cache.get(session).saver.save(onUpdate); - } +export async function saveAnnotations(session, onUpdate) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - // If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it + if (cache.has(session)) { + await cache.get(session).saver.save(onUpdate); } - function searchAnnotations(session, filters, frameFrom, frameTo) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + // If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it +} - if (cache.has(session)) { - return cache.get(session).collection.search(filters, frameFrom, frameTo); - } +export function searchAnnotations(session, filters, frameFrom, frameTo) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).collection.search(filters, frameFrom, frameTo); } - function searchEmptyFrame(session, frameFrom, frameTo) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).collection.searchEmpty(frameFrom, frameTo); - } +export function searchEmptyFrame(session, frameFrom, frameTo) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).collection.searchEmpty(frameFrom, frameTo); } - function mergeAnnotations(session, objectStates) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).collection.merge(objectStates); - } +export function mergeAnnotations(session, objectStates) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).collection.merge(objectStates); } - function splitAnnotations(session, objectState, frame) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).collection.split(objectState, frame); - } +export function splitAnnotations(session, objectState, frame) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).collection.split(objectState, frame); } - function groupAnnotations(session, objectStates, reset) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).collection.group(objectStates, reset); - } +export function groupAnnotations(session, objectStates, reset) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).collection.group(objectStates, reset); } - function hasUnsavedChanges(session) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).saver.hasUnsavedChanges(); - } +export function hasUnsavedChanges(session) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - return false; + if (cache.has(session)) { + return cache.get(session).saver.hasUnsavedChanges(); } - async function clearAnnotations(session, reload, startframe, endframe, delTrackKeyframesOnly) { - checkObjectType('reload', reload, 'boolean', null); - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + return false; +} - if (cache.has(session)) { - cache.get(session).collection.clear(startframe, endframe, delTrackKeyframesOnly); - } +export async function clearAnnotations(session, reload, startframe, endframe, delTrackKeyframesOnly) { + checkObjectType('reload', reload, 'boolean', null); + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - if (reload) { - cache.delete(session); - await getAnnotationsFromServer(session); - } + if (cache.has(session)) { + cache.get(session).collection.clear(startframe, endframe, delTrackKeyframesOnly); } - function annotationsStatistics(session) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + if (reload) { + cache.delete(session); + await getAnnotationsFromServer(session); + } +} - if (cache.has(session)) { - return cache.get(session).collection.statistics(); - } +export function annotationsStatistics(session) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).collection.statistics(); } - function putAnnotations(session, objectStates) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).collection.put(objectStates); - } +export function putAnnotations(session, objectStates) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).collection.put(objectStates); } - function selectObject(session, objectStates, x, y) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).collection.select(objectStates, x, y); - } +export function selectObject(session, objectStates, x, y) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).collection.select(objectStates, x, y); } - async function uploadAnnotations(session, file, loader) { - const sessionType = session instanceof Task ? 'task' : 'job'; - if (!(loader instanceof Loader)) { - throw new ArgumentError('A loader must be instance of Loader class'); - } - await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, loader.name); - } + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - function importAnnotations(session, data) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); +export function importCollection(session, data) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - if (cache.has(session)) { - // eslint-disable-next-line no-unsanitized/method - return cache.get(session).collection.import(data); - } - - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + // eslint-disable-next-line no-unsanitized/method + return cache.get(session).collection.import(data); } - function exportAnnotations(session) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).collection.export(); - } +export function exportCollection(session) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).collection.export(); } - async function exportDataset(instance, format, name, saveImages = false) { - if (!(format instanceof String || typeof format === 'string')) { - throw new ArgumentError('Format must be a string'); - } - if (!(instance instanceof Task || instance instanceof Project || instance instanceof Job)) { - throw new ArgumentError('A dataset can only be created from a job, task or project'); - } - if (typeof saveImages !== 'boolean') { - throw new ArgumentError('Save images parameter must be a boolean'); - } + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} + +export async function exportDataset( + instance, + format: string, + saveImages: boolean, + useDefaultSettings: boolean, + targetStorage: Storage, + name?: string, +) { + if (!(instance instanceof Task || instance instanceof Project || instance instanceof Job)) { + throw new ArgumentError('A dataset can only be created from a job, task or project'); + } - let result = null; - if (instance instanceof Task) { - result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages); - } else if (instance instanceof Job) { - result = await serverProxy.tasks.exportDataset(instance.taskId, format, name, saveImages); - } else { - result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages); - } + let result = null; + if (instance instanceof Task) { + result = await serverProxy.tasks + .exportDataset(instance.id, format, saveImages, useDefaultSettings, targetStorage, name); + } else if (instance instanceof Job) { + result = await serverProxy.jobs + .exportDataset(instance.id, format, saveImages, useDefaultSettings, targetStorage, name); + } else { + result = await serverProxy.projects + .exportDataset(instance.id, format, saveImages, useDefaultSettings, targetStorage, name); + } - return result; + return result; +} + +export function importDataset( + instance: any, + format: string, + useDefaultSettings: boolean, + sourceStorage: Storage, + file: File | string, + updateStatusCallback = () => {}, +) { + if (!(instance instanceof Project || instance instanceof Task || instance instanceof Job)) { + throw new ArgumentError('Instance should be a Project || Task || Job instance'); + } + if (!(typeof updateStatusCallback === 'function')) { + throw new ArgumentError('Callback should be a function'); + } + if (typeof file === 'string' && !file.toLowerCase().endsWith('.zip')) { + throw new ArgumentError('File should be file instance with ZIP extension'); + } + if (file instanceof File && !(['application/zip', 'application/x-zip-compressed'].includes(file.type))) { + throw new ArgumentError('File should be file instance with ZIP extension'); } - function importDataset(instance, format, file, updateStatusCallback = () => {}) { - if (!(typeof format === 'string')) { - throw new ArgumentError('Format must be a string'); - } - if (!(instance instanceof Project)) { - throw new ArgumentError('Instance should be a Project instance'); - } - if (!(typeof updateStatusCallback === 'function')) { - throw new ArgumentError('Callback should be a function'); - } - if (!(['application/zip', 'application/x-zip-compressed'].includes(file.type))) { - throw new ArgumentError('File should be file instance with ZIP extension'); - } - return serverProxy.projects.importDataset(instance.id, format, file, updateStatusCallback); + if (instance instanceof Project) { + return serverProxy.projects + .importDataset(instance.id, format, useDefaultSettings, sourceStorage, file, updateStatusCallback); } - function getHistory(session) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + const instanceType = instance instanceof Task ? 'task' : 'job'; + return serverProxy.annotations + .uploadAnnotations(instanceType, instance.id, format, useDefaultSettings, sourceStorage, file); +} - if (cache.has(session)) { - return cache.get(session).history; - } +export function getHistory(session) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).history; } - async function undoActions(session, count) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).history.undo(count); - } +export async function undoActions(session, count) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).history.undo(count); } - async function redoActions(session, count) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).history.redo(count); - } +export async function redoActions(session, count) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).history.redo(count); } - function freezeHistory(session, frozen) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).history.freeze(frozen); - } +export function freezeHistory(session, frozen) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).history.freeze(frozen); } - function clearActions(session) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).history.clear(); - } +export function clearActions(session) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); + if (cache.has(session)) { + return cache.get(session).history.clear(); } - function getActions(session) { - const sessionType = session instanceof Task ? 'task' : 'job'; - const cache = getCache(sessionType); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} - if (cache.has(session)) { - return cache.get(session).history.get(); - } +export function getActions(session) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); + + if (cache.has(session)) { + return cache.get(session).history.get(); + } - throw new DataError( - 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', - ); - } - - module.exports = { - getAnnotations, - putAnnotations, - saveAnnotations, - hasUnsavedChanges, - mergeAnnotations, - searchAnnotations, - searchEmptyFrame, - splitAnnotations, - groupAnnotations, - clearAnnotations, - annotationsStatistics, - selectObject, - uploadAnnotations, - importAnnotations, - exportAnnotations, - exportDataset, - importDataset, - undoActions, - redoActions, - freezeHistory, - getHistory, - clearActions, - getActions, - closeSession, - }; -})(); + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); +} diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 392ea012..2a9221e2 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -6,7 +6,7 @@ const config = require('./config'); (() => { const PluginRegistry = require('./plugins').default; - const serverProxy = require('./server-proxy'); + const serverProxy = require('./server-proxy').default; const lambdaManager = require('./lambda-manager'); const { isBoolean, @@ -21,7 +21,7 @@ const config = require('./config'); const { AnnotationFormats } = require('./annotation-formats'); const { ArgumentError } = require('./exceptions'); const { Task, Job } = require('./session'); - const { Project } = require('./project'); + const Project = require('./project').default; const { CloudStorage } = require('./cloud-storage'); const Organization = require('./organization'); diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index 77f1eb00..59bb6204 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -16,8 +16,8 @@ function build() { const Comment = require('./comment'); const Issue = require('./issue'); const { Job, Task } = require('./session'); - const { Project } = require('./project'); - const implementProject = require('./project-implementation'); + const Project = require('./project').default; + const implementProject = require('./project-implementation').default; const { Attribute, Label } = require('./labels'); const MLModel = require('./ml-model'); const { FrameData } = require('./frames'); diff --git a/cvat-core/src/cloud-storage.ts b/cvat-core/src/cloud-storage.ts index ab2c70d8..94f8c02e 100644 --- a/cvat-core/src/cloud-storage.ts +++ b/cvat-core/src/cloud-storage.ts @@ -4,7 +4,7 @@ (() => { const PluginRegistry = require('./plugins').default; - const serverProxy = require('./server-proxy'); + const serverProxy = require('./server-proxy').default; const { isBrowser, isNode } = require('browser-or-node'); const { ArgumentError } = require('./exceptions'); const { CloudStorageCredentialsType, CloudStorageProviderType } = require('./enums'); diff --git a/cvat-core/src/enums.ts b/cvat-core/src/enums.ts index 8bee70be..1f1ebd98 100644 --- a/cvat-core/src/enums.ts +++ b/cvat-core/src/enums.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier = MIT @@ -389,7 +390,7 @@ export enum CloudStorageCredentialsType { } /** - * Task statuses + * Membership roles * @enum {string} * @name MembershipRole * @memberof module:API.cvat.enums @@ -423,3 +424,17 @@ export enum SortingMethod { PREDEFINED = 'predefined', RANDOM = 'random', } + +/** + * Types of storage locations + * @enum {string} + * @name StorageLocation + * @memberof module:API.cvat.enums + * @property {string} LOCAL 'local' + * @property {string} CLOUD_STORAGE 'cloud_storage' + * @readonly +*/ +export enum StorageLocation { + LOCAL = 'local', + CLOUD_STORAGE = 'cloud_storage', +} diff --git a/cvat-core/src/exceptions.ts b/cvat-core/src/exceptions.ts index db306115..88eaee74 100644 --- a/cvat-core/src/exceptions.ts +++ b/cvat-core/src/exceptions.ts @@ -171,7 +171,7 @@ export class Exception extends Error { }; try { - const serverProxy = require('./server-proxy'); + const serverProxy = require('./server-proxy').default; await serverProxy.server.exception(exceptionObject); } catch (exception) { // add event diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts index 4dfd9ae0..048c1fe2 100644 --- a/cvat-core/src/frames.ts +++ b/cvat-core/src/frames.ts @@ -5,7 +5,7 @@ (() => { const cvatData = require('cvat-data'); const PluginRegistry = require('./plugins').default; - const serverProxy = require('./server-proxy'); + const serverProxy = require('./server-proxy').default; const { isBrowser, isNode } = require('browser-or-node'); const { Exception, ArgumentError, DataError } = require('./exceptions'); diff --git a/cvat-core/src/issue.ts b/cvat-core/src/issue.ts index 795d5b7a..9d6b5c2b 100644 --- a/cvat-core/src/issue.ts +++ b/cvat-core/src/issue.ts @@ -8,7 +8,7 @@ const PluginRegistry = require('./plugins').default; const Comment = require('./comment'); const User = require('./user'); const { ArgumentError } = require('./exceptions'); -const serverProxy = require('./server-proxy'); +const serverProxy = require('./server-proxy').default; /** * Class representing a single issue diff --git a/cvat-core/src/lambda-manager.ts b/cvat-core/src/lambda-manager.ts index b7562dab..51a85710 100644 --- a/cvat-core/src/lambda-manager.ts +++ b/cvat-core/src/lambda-manager.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -const serverProxy = require('./server-proxy'); +const serverProxy = require('./server-proxy').default; const { ArgumentError } = require('./exceptions'); const MLModel = require('./ml-model'); const { RQStatus } = require('./enums'); diff --git a/cvat-core/src/logger-storage.ts b/cvat-core/src/logger-storage.ts index 530219f9..82229372 100644 --- a/cvat-core/src/logger-storage.ts +++ b/cvat-core/src/logger-storage.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT const PluginRegistry = require('./plugins').default; -const serverProxy = require('./server-proxy'); +const serverProxy = require('./server-proxy').default; const logFactory = require('./log'); const { ArgumentError } = require('./exceptions'); const { LogType } = require('./enums'); diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index 4fcf129b..4a304ca5 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -7,7 +7,7 @@ const config = require('./config'); const { MembershipRole } = require('./enums'); const { ArgumentError, ServerError } = require('./exceptions'); const PluginRegistry = require('./plugins').default; -const serverProxy = require('./server-proxy'); +const serverProxy = require('./server-proxy').default; const User = require('./user'); /** diff --git a/cvat-core/src/project-implementation.ts b/cvat-core/src/project-implementation.ts index fb6445dc..efef65a7 100644 --- a/cvat-core/src/project-implementation.ts +++ b/cvat-core/src/project-implementation.ts @@ -1,93 +1,108 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -(() => { - const serverProxy = require('./server-proxy'); - const { getPreview } = require('./frames'); - - const { Project } = require('./project'); - const { exportDataset, importDataset } = require('./annotations'); - - function implementProject(projectClass) { - projectClass.prototype.save.implementation = async function () { - if (typeof this.id !== 'undefined') { - const projectData = this._updateTrigger.getUpdated(this, { - bugTracker: 'bug_tracker', - trainingProject: 'training_project', - assignee: 'assignee_id', - }); - if (projectData.assignee_id) { - projectData.assignee_id = projectData.assignee_id.id; - } - if (projectData.labels) { - projectData.labels = projectData.labels.map((el) => el.toJSON()); - } - - await serverProxy.projects.save(this.id, projectData); - this._updateTrigger.reset(); - return this; - } +import { Storage } from './storage'; - // initial creating - const projectSpec = { - name: this.name, - labels: this.labels.map((el) => el.toJSON()), - }; +const serverProxy = require('./server-proxy').default; +const { getPreview } = require('./frames'); - if (this.bugTracker) { - projectSpec.bug_tracker = this.bugTracker; - } +const Project = require('./project').default; +const { exportDataset, importDataset } = require('./annotations'); - if (this.trainingProject) { - projectSpec.training_project = this.trainingProject; +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', + trainingProject: 'training_project', + assignee: 'assignee_id', + }); + if (projectData.assignee_id) { + projectData.assignee_id = projectData.assignee_id.id; + } + if (projectData.labels) { + projectData.labels = projectData.labels.map((el) => el.toJSON()); } - const project = await serverProxy.projects.create(projectSpec); - return new Project(project); - }; + await serverProxy.projects.save(this.id, projectData); + this._updateTrigger.reset(); + return this; + } - projectClass.prototype.delete.implementation = async function () { - const result = await serverProxy.projects.delete(this.id); - return result; + // initial creating + const projectSpec: any = { + name: this.name, + labels: this.labels.map((el) => el.toJSON()), }; - projectClass.prototype.preview.implementation = async function () { - if (!this._internalData.task_ids.length) { - return ''; - } - const frameData = await getPreview(this._internalData.task_ids[0]); - return frameData; - }; + if (this.bugTracker) { + projectSpec.bug_tracker = this.bugTracker; + } - projectClass.prototype.annotations.exportDataset.implementation = async function ( - format, - saveImages, - customName, - ) { - const result = exportDataset(this, format, customName, saveImages); - return result; - }; - projectClass.prototype.annotations.importDataset.implementation = async function ( - format, - file, - updateStatusCallback, - ) { - return importDataset(this, format, file, updateStatusCallback); - }; + if (this.trainingProject) { + projectSpec.training_project = this.trainingProject; + } - projectClass.prototype.backup.implementation = async function () { - const result = await serverProxy.projects.backupProject(this.id); - return result; - }; + if (this.targetStorage) { + projectSpec.target_storage = this.targetStorage.toJSON(); + } - projectClass.restore.implementation = async function (file) { - const result = await serverProxy.projects.restoreProject(file); - return result.id; - }; + if (this.sourceStorage) { + projectSpec.source_storage = this.sourceStorage.toJSON(); + } + + const project = await serverProxy.projects.create(projectSpec); + return new Project(project); + }; + + projectClass.prototype.delete.implementation = async function () { + const result = await serverProxy.projects.delete(this.id); + return result; + }; + + projectClass.prototype.preview.implementation = async function () { + if (!this._internalData.task_ids.length) { + return ''; + } + const frameData = await getPreview(this._internalData.task_ids[0]); + return frameData; + }; + + 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, + updateStatusCallback, + ) { + return importDataset(this, format, useDefaultSettings, sourceStorage, file, updateStatusCallback); + }; + + 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; + }; - return projectClass; - } + projectClass.restore.implementation = async function (storage: Storage, file: File | string) { + const result = await serverProxy.projects.restore(storage, file); + return result; + }; - module.exports = implementProject; -})(); + return projectClass; +} diff --git a/cvat-core/src/project.ts b/cvat-core/src/project.ts index 01e63e3a..4e4cede0 100644 --- a/cvat-core/src/project.ts +++ b/cvat-core/src/project.ts @@ -1,374 +1,429 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -(() => { - const PluginRegistry = require('./plugins').default; - const { ArgumentError } = require('./exceptions'); - const { Label } = require('./labels'); - const User = require('./user'); - const { FieldUpdateTrigger } = require('./common'); +import { StorageLocation } from './enums'; +import { Storage } from './storage'; +const PluginRegistry = require('./plugins').default; +const { ArgumentError } = require('./exceptions'); +const { Label } = require('./labels'); +const User = require('./user'); +const { FieldUpdateTrigger } = require('./common'); + +/** + * Class representing a project + * @memberof module:API.cvat.classes + */ +export default class Project { /** - * Class representing a project - * @memberof module:API.cvat.classes + * In a fact you need use the constructor only if you want to create a project + * @param {object} initialData - Object which is used for initialization + *
It can contain keys: + *
  • name + *
  • labels */ - class Project { - /** - * In a fact you need use the constructor only if you want to create a project - * @param {object} initialData - Object which is used for initialization - *
    It can contain keys: - *
  • name - *
  • labels - */ - constructor(initialData) { - const data = { - id: undefined, - name: undefined, - status: undefined, - assignee: undefined, - owner: undefined, - bug_tracker: undefined, - created_date: undefined, - updated_date: undefined, - task_subsets: undefined, - training_project: undefined, - task_ids: undefined, - dimension: undefined, - }; + constructor(initialData) { + const data = { + id: undefined, + name: undefined, + status: undefined, + assignee: undefined, + owner: undefined, + bug_tracker: undefined, + created_date: undefined, + updated_date: undefined, + task_subsets: undefined, + training_project: undefined, + task_ids: undefined, + dimension: undefined, + source_storage: undefined, + target_storage: undefined, + labels: undefined, + }; - const updateTrigger = new FieldUpdateTrigger(); + const updateTrigger = new FieldUpdateTrigger(); - for (const property in data) { - if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { - data[property] = initialData[property]; - } + for (const property in data) { + if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { + data[property] = initialData[property]; } + } - data.labels = []; + data.labels = []; - if (Array.isArray(initialData.labels)) { - data.labels = initialData.labels - .map((labelData) => new Label(labelData)).filter((label) => !label.hasParent); - } + if (Array.isArray(initialData.labels)) { + data.labels = initialData.labels + .map((labelData) => new Label(labelData)).filter((label) => !label.hasParent); + } - if (typeof initialData.training_project === 'object') { - data.training_project = { ...initialData.training_project }; - } + if (typeof initialData.training_project === 'object') { + data.training_project = { ...initialData.training_project }; + } - Object.defineProperties( - this, - Object.freeze({ - /** - * @name id - * @type {number} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - id: { - get: () => data.id, - }, - /** - * @name name - * @type {string} - * @memberof module:API.cvat.classes.Project - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - name: { - get: () => data.name, - set: (value) => { - if (!value.trim().length) { - throw new ArgumentError('Value must not be empty'); - } - data.name = value; - updateTrigger.update('name'); - }, + Object.defineProperties( + this, + Object.freeze({ + /** + * @name id + * @type {number} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + id: { + get: () => data.id, + }, + /** + * @name name + * @type {string} + * @memberof module:API.cvat.classes.Project + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + name: { + get: () => data.name, + set: (value) => { + if (!value.trim().length) { + throw new ArgumentError('Value must not be empty'); + } + data.name = value; + updateTrigger.update('name'); }, + }, - /** - * @name status - * @type {module:API.cvat.enums.TaskStatus} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - status: { - get: () => data.status, - }, - /** - * Instance of a user who was assigned for the project - * @name assignee - * @type {module:API.cvat.classes.User} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - assignee: { - get: () => data.assignee, - set: (assignee) => { - if (assignee !== null && !(assignee instanceof User)) { - throw new ArgumentError('Value must be a user instance'); - } - data.assignee = assignee; - updateTrigger.update('assignee'); - }, - }, - /** - * Instance of a user who has created the project - * @name owner - * @type {module:API.cvat.classes.User} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - owner: { - get: () => data.owner, - }, - /** - * @name bugTracker - * @type {string} - * @memberof module:API.cvat.classes.Project - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - bugTracker: { - get: () => data.bug_tracker, - set: (tracker) => { - data.bug_tracker = tracker; - updateTrigger.update('bugTracker'); - }, - }, - /** - * @name createdDate - * @type {string} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - createdDate: { - get: () => data.created_date, - }, - /** - * @name updatedDate - * @type {string} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - updatedDate: { - get: () => data.updated_date, + /** + * @name status + * @type {module:API.cvat.enums.TaskStatus} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + status: { + get: () => data.status, + }, + /** + * Instance of a user who was assigned for the project + * @name assignee + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + assignee: { + get: () => data.assignee, + set: (assignee) => { + if (assignee !== null && !(assignee instanceof User)) { + throw new ArgumentError('Value must be a user instance'); + } + data.assignee = assignee; + updateTrigger.update('assignee'); }, - /** - * Dimesion of the tasks in the project, if no task dimension is null - * @name dimension - * @type {string} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - dimension: { - get: () => data.dimension, + }, + /** + * Instance of a user who has created the project + * @name owner + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + owner: { + get: () => data.owner, + }, + /** + * @name bugTracker + * @type {string} + * @memberof module:API.cvat.classes.Project + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + bugTracker: { + get: () => data.bug_tracker, + set: (tracker) => { + data.bug_tracker = tracker; + updateTrigger.update('bugTracker'); }, - /** - * After project has been created value can be appended only. - * @name labels - * @type {module:API.cvat.classes.Label[]} - * @memberof module:API.cvat.classes.Project - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - labels: { - get: () => [...data.labels], - set: (labels) => { - if (!Array.isArray(labels)) { - throw new ArgumentError('Value must be an array of Labels'); - } + }, + /** + * @name createdDate + * @type {string} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + createdDate: { + get: () => data.created_date, + }, + /** + * @name updatedDate + * @type {string} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + updatedDate: { + get: () => data.updated_date, + }, + /** + * Dimesion of the tasks in the project, if no task dimension is null + * @name dimension + * @type {string} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + dimension: { + get: () => data.dimension, + }, + /** + * After project has been created value can be appended only. + * @name labels + * @type {module:API.cvat.classes.Label[]} + * @memberof module:API.cvat.classes.Project + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + labels: { + get: () => [...data.labels], + set: (labels) => { + if (!Array.isArray(labels)) { + throw new ArgumentError('Value must be an array of Labels'); + } - if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) { - throw new ArgumentError( - `Each array value must be an instance of Label. ${typeof label} was found`, - ); - } + if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) { + throw new ArgumentError( + `Each array value must be an instance of Label. ${typeof label} was found`, + ); + } - const IDs = labels.map((_label) => _label.id); - const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id)); - deletedLabels.forEach((_label) => { - _label.deleted = true; - }); + const IDs = labels.map((_label) => _label.id); + const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id)); + deletedLabels.forEach((_label) => { + _label.deleted = true; + }); - data.labels = [...deletedLabels, ...labels]; - updateTrigger.update('labels'); - }, - }, - /** - * Subsets array for related tasks - * @name subsets - * @type {string[]} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - subsets: { - get: () => [...data.task_subsets], + data.labels = [...deletedLabels, ...labels]; + updateTrigger.update('labels'); }, - /** - * Training project associated with this annotation project - * This is a simple object which contains - * keys like host, username, password, enabled, project_class - * @name trainingProject - * @type {object} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - trainingProject: { - get: () => { - if (typeof data.training_project === 'object') { - return { ...data.training_project }; - } - return data.training_project; - }, - set: (updatedProject) => { - if (typeof training === 'object') { - data.training_project = { ...updatedProject }; - } else { - data.training_project = updatedProject; - } - updateTrigger.update('trainingProject'); - }, - }, - _internalData: { - get: () => data, + }, + /** + * Subsets array for related tasks + * @name subsets + * @type {string[]} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + subsets: { + get: () => [...data.task_subsets], + }, + /** + * Training project associated with this annotation project + * This is a simple object which contains + * keys like host, username, password, enabled, project_class + * @name trainingProject + * @type {object} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + trainingProject: { + get: () => { + if (typeof data.training_project === 'object') { + return { ...data.training_project }; + } + return data.training_project; }, - _updateTrigger: { - get: () => updateTrigger, + set: (updatedProject) => { + if (typeof training === 'object') { + data.training_project = { ...updatedProject }; + } else { + data.training_project = updatedProject; + } + updateTrigger.update('trainingProject'); }, - }), - ); + }, + /** + * Source storage for import resources. + * @name sourceStorage + * @type {module:API.cvat.classes.Storage} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + sourceStorage: { + get: () => ( + new Storage({ + location: data.source_storage?.location || StorageLocation.LOCAL, + cloudStorageId: data.source_storage?.cloud_storage_id, + }) + ), + }, + /** + * Target storage for export resources. + * @name targetStorage + * @type {module:API.cvat.classes.Storage} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + targetStorage: { + get: () => ( + new Storage({ + location: data.target_storage?.location || StorageLocation.LOCAL, + cloudStorageId: data.target_storage?.cloud_storage_id, + }) + ), + }, + _internalData: { + get: () => data, + }, + _updateTrigger: { + get: () => updateTrigger, + }, + }), + ); - // When we call a function, for example: project.annotations.get() - // In the method get we lose the project context - // So, we need return it - this.annotations = { - exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), - importDataset: Object.getPrototypeOf(this).annotations.importDataset.bind(this), - }; - } + // When we call a function, for example: project.annotations.get() + // In the method get we lose the project context + // So, we need return it + this.annotations = { + exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), + importDataset: Object.getPrototypeOf(this).annotations.importDataset.bind(this), + }; + } - /** - * Get the first frame of the first task of a project for preview - * @method preview - * @memberof Project - * @returns {string} - jpeg encoded image - * @instance - * @async - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - async preview() { - const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.preview); - return result; - } + /** + * Get the first frame of the first task of a project for preview + * @method preview + * @memberof Project + * @returns {string} - jpeg encoded image + * @instance + * @async + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + async preview() { + const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.preview); + return result; + } - /** - * Method updates data of a created project or creates new project from scratch - * @method save - * @returns {module:API.cvat.classes.Project} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - * @async - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.PluginError} - */ - async save() { - const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save); - return result; - } + /** + * Method updates data of a created project or creates new project from scratch + * @method save + * @returns {module:API.cvat.classes.Project} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async save() { + const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save); + return result; + } - /** - * Method deletes a project from a server - * @method delete - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - * @async - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.PluginError} - */ - async delete() { - const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete); - return result; - } + /** + * Method deletes a project from a server + * @method delete + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async delete() { + const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete); + return result; + } - /** - * Method makes a backup of a project - * @method export - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - * @async - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.PluginError} - * @returns {string} URL to get result archive - */ - async backup() { - const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.backup); - return result; - } + /** + * Method makes a backup of a project + * @method backup + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + * @returns {string} URL to get result archive + */ + async backup(targetStorage: Storage, useDefaultSettings: boolean, fileName?: string) { + const result = await PluginRegistry.apiWrapper.call( + this, + Project.prototype.backup, + targetStorage, + useDefaultSettings, + fileName, + ); + return result; + } - /** - * Method restores a project from a backup - * @method restore - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - * @async - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.PluginError} - * @returns {number} ID of the imported project - */ - static async restore(file) { - const result = await PluginRegistry.apiWrapper.call(this, Project.restore, file); - return result; - } + /** + * Method restores a project from a backup + * @method restore + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + * @returns {number} ID of the imported project + */ + static async restore(storage: Storage, file: File | string) { + const result = await PluginRegistry.apiWrapper.call(this, Project.restore, storage, file); + return result; } +} - Object.defineProperties( - Project.prototype, - Object.freeze({ - annotations: Object.freeze({ - value: { - async exportDataset(format, saveImages, customName = '') { - const result = await PluginRegistry.apiWrapper.call( - this, - Project.prototype.annotations.exportDataset, - format, - saveImages, - customName, - ); - return result; - }, - async importDataset(format, file, updateStatusCallback = null) { - const result = await PluginRegistry.apiWrapper.call( - this, - Project.prototype.annotations.importDataset, - format, - file, - updateStatusCallback, - ); - return result; - }, +Object.defineProperties( + Project.prototype, + Object.freeze({ + annotations: Object.freeze({ + value: { + async exportDataset( + format: string, + saveImages: boolean, + useDefaultSettings: boolean, + targetStorage: Storage, + customName?: string, + ) { + const result = await PluginRegistry.apiWrapper.call( + this, + Project.prototype.annotations.exportDataset, + format, + saveImages, + useDefaultSettings, + targetStorage, + customName, + ); + return result; }, - writable: true, - }), + async importDataset( + format: string, + useDefaultSettings: boolean, + sourceStorage: Storage, + file: File | string, + updateStatusCallback = null, + ) { + const result = await PluginRegistry.apiWrapper.call( + this, + Project.prototype.annotations.importDataset, + format, + useDefaultSettings, + sourceStorage, + file, + updateStatusCallback, + ); + return result; + }, + }, + writable: true, }), - ); - - module.exports = { - Project, - }; -})(); + }), +); diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index bcc7e3ad..d4441c63 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -1,639 +1,722 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -(() => { - const FormData = require('form-data'); - const { ServerError } = require('./exceptions'); - const store = require('store'); - const config = require('./config'); - const DownloadWorker = require('./download.worker'); - const Axios = require('axios'); - const tus = require('tus-js-client'); - - function enableOrganization() { - return { org: config.organizationID || '' }; - } - - function removeToken() { - Axios.defaults.headers.common.Authorization = ''; - store.remove('token'); - } +import { StorageLocation } from './enums'; +import { Storage } from './storage'; + +type Params = { + org: number | string, + use_default_location?: boolean, + location?: StorageLocation, + cloud_storage_id?: number, + format?: string, + filename?: string, + action?: string, +}; + +const FormData = require('form-data'); +const store = require('store'); +const config = require('./config'); +const DownloadWorker = require('./download.worker'); +const { ServerError } = require('./exceptions'); +const Axios = require('axios'); +const tus = require('tus-js-client'); + + +function enableOrganization() { + return { org: config.organizationID || '' }; +} + +function configureStorage(storage: Storage, useDefaultLocation = false): Partial { + return { + use_default_location: useDefaultLocation, + ...(!useDefaultLocation ? { + location: storage.location, + ...(storage.cloudStorageId ? { + cloud_storage_id: storage.cloudStorageId, + } : {}), + } : {}), + }; +} + +function removeToken() { + Axios.defaults.headers.common.Authorization = ''; + store.remove('token'); +} + +function waitFor(frequencyHz, predicate) { + return new Promise((resolve, reject) => { + if (typeof predicate !== 'function') { + reject(new Error(`Predicate must be a function, got ${typeof predicate}`)); + } - function waitFor(frequencyHz, predicate) { - return new Promise((resolve, reject) => { - if (typeof predicate !== 'function') { - reject(new Error(`Predicate must be a function, got ${typeof predicate}`)); + const internalWait = () => { + let result = false; + try { + result = predicate(); + } catch (error) { + reject(error); } - const internalWait = () => { - let result = false; - try { - result = predicate(); - } catch (error) { - reject(error); - } - - if (result) { - resolve(); - } else { - setTimeout(internalWait, 1000 / frequencyHz); - } - }; - - setTimeout(internalWait); + if (result) { + resolve(); + } else { + setTimeout(internalWait, 1000 / frequencyHz); + } + }; + + setTimeout(internalWait); + }); +} + +async function chunkUpload(file, uploadConfig) { + const params = enableOrganization(); + const { + endpoint, chunkSize, totalSize, onUpdate, metadata, + } = uploadConfig; + const { totalSentSize } = uploadConfig; + const uploadResult = { totalSentSize }; + return new Promise((resolve, reject) => { + const upload = new tus.Upload(file, { + endpoint, + metadata: { + filename: file.name, + filetype: file.type, + ...metadata, + }, + headers: { + Authorization: Axios.defaults.headers.common.Authorization, + }, + chunkSize, + retryDelays: null, + onError(error) { + reject(error); + }, + onBeforeRequest(req) { + const xhr = req.getUnderlyingObject(); + const { org } = params; + req.setHeader('X-Organization', org); + xhr.withCredentials = true; + }, + onProgress(bytesUploaded) { + if (onUpdate && Number.isInteger(totalSentSize) && Number.isInteger(totalSize)) { + const currentUploadedSize = totalSentSize + bytesUploaded; + const percentage = currentUploadedSize / totalSize; + onUpdate(percentage); + } + }, + onAfterResponse(request, response) { + const uploadFilename = response.getHeader('Upload-Filename'); + if (uploadFilename) uploadResult.filename = uploadFilename; + }, + onSuccess() { + if (totalSentSize) uploadResult.totalSentSize += file.size; + resolve(uploadResult); + }, }); + upload.start(); + }); +} + +function generateError(errorData) { + if (errorData.response) { + const message = `${errorData.message}. ${JSON.stringify(errorData.response.data) || ''}.`; + return new ServerError(message, errorData.response.status); } - async function chunkUpload(file, uploadConfig) { - const params = enableOrganization(); - const { - endpoint, chunkSize, totalSize, onUpdate, metadata, - } = uploadConfig; - const { totalSentSize } = uploadConfig; - const uploadResult = { totalSentSize }; - return new Promise((resolve, reject) => { - const upload = new tus.Upload(file, { - endpoint, - metadata: { - filename: file.name, - filetype: file.type, - ...metadata, - }, - headers: { - Authorization: Axios.defaults.headers.common.Authorization, - }, - chunkSize, - retryDelays: null, - onError(error) { - reject(error); - }, - onBeforeRequest(req) { - const xhr = req.getUnderlyingObject(); - const { org } = params; - req.setHeader('X-Organization', org); - xhr.withCredentials = true; - }, - onProgress(bytesUploaded) { - if (onUpdate && Number.isInteger(totalSentSize) && Number.isInteger(totalSize)) { - const currentUploadedSize = totalSentSize + bytesUploaded; - const percentage = currentUploadedSize / totalSize; - onUpdate(percentage); - } - }, - onAfterResponse(request, response) { - const uploadFilename = response.getHeader('Upload-Filename'); - if (uploadFilename) uploadResult.filename = uploadFilename; - }, - onSuccess() { - if (totalSentSize) uploadResult.totalSentSize += file.size; - resolve(uploadResult); - }, + // Server is unavailable (no any response) + const message = `${errorData.message}.`; // usually is "Error Network" + return new ServerError(message, 0); +} + +function prepareData(details) { + const data = new FormData(); + for (const [key, value] of Object.entries(details)) { + if (Array.isArray(value)) { + value.forEach((element, idx) => { + data.append(`${key}[${idx}]`, element); }); - upload.start(); - }); - } - - function generateError(errorData) { - if (errorData.response) { - const message = `${errorData.message}. ${JSON.stringify(errorData.response.data) || ''}.`; - return new ServerError(message, errorData.response.status); + } else { + data.set(key, value); } - - // Server is unavailable (no any response) - const message = `${errorData.message}.`; // usually is "Error Network" - return new ServerError(message, 0); } + return data; +} + +class WorkerWrappedAxios { + constructor(requestInterseptor) { + const worker = new DownloadWorker(requestInterseptor); + const requests = {}; + let requestId = 0; + + worker.onmessage = (e) => { + if (e.data.id in requests) { + if (e.data.isSuccess) { + requests[e.data.id].resolve(e.data.responseData); + } else { + requests[e.data.id].reject({ + response: { + status: e.data.status, + data: e.data.responseData, + }, + }); + } - function prepareData(details) { - const data = new FormData(); - for (const [key, value] of Object.entries(details)) { - if (Array.isArray(value)) { - value.forEach((element, idx) => { - data.append(`${key}[${idx}]`, element); - }); - } else { - data.set(key, value); + delete requests[e.data.id]; + } + }; + + worker.onerror = (e) => { + if (e.data.id in requests) { + requests[e.data.id].reject(e); + delete requests[e.data.id]; } + }; + + function getRequestId() { + return requestId++; } - return data; - } - class WorkerWrappedAxios { - constructor(requestInterseptor) { - const worker = new DownloadWorker(requestInterseptor); - const requests = {}; - let requestId = 0; + async function get(url, requestConfig) { + return new Promise((resolve, reject) => { + const newRequestId = getRequestId(); + requests[newRequestId] = { + resolve, + reject, + }; + worker.postMessage({ + url, + config: requestConfig, + id: newRequestId, + }); + }); + } - worker.onmessage = (e) => { - if (e.data.id in requests) { - if (e.data.isSuccess) { - requests[e.data.id].resolve(e.data.responseData); - } else { - requests[e.data.id].reject({ - response: { - status: e.data.status, - data: e.data.responseData, - }, - }); - } + Object.defineProperties( + this, + Object.freeze({ + get: { + value: get, + writable: false, + }, + }), + ); + } +} + +class ServerProxy { + constructor() { + Axios.defaults.withCredentials = true; + Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'; + Axios.defaults.xsrfCookieName = 'csrftoken'; + const workerAxios = new WorkerWrappedAxios(); + Axios.interceptors.request.use((reqConfig) => { + if ('params' in reqConfig && 'org' in reqConfig.params) { + return reqConfig; + } - delete requests[e.data.id]; - } - }; + reqConfig.params = { ...enableOrganization(), ...(reqConfig.params || {}) }; + return reqConfig; + }); - worker.onerror = (e) => { - if (e.data.id in requests) { - requests[e.data.id].reject(e); - delete requests[e.data.id]; - } - }; + let token = store.get('token'); + if (token) { + Axios.defaults.headers.common.Authorization = `Token ${token}`; + } - function getRequestId() { - return requestId++; - } + async function about() { + const { backendAPI } = config; - async function get(url, requestConfig) { - return new Promise((resolve, reject) => { - const newRequestId = getRequestId(); - requests[newRequestId] = { - resolve, - reject, - }; - worker.postMessage({ - url, - config: requestConfig, - id: newRequestId, - }); + let response = null; + try { + response = await Axios.get(`${backendAPI}/server/about`, { + proxy: config.proxy, }); + } catch (errorData) { + throw generateError(errorData); } - Object.defineProperties( - this, - Object.freeze({ - get: { - value: get, - writable: false, - }, - }), - ); + return response.data; } - } - - class ServerProxy { - constructor() { - Axios.defaults.withCredentials = true; - Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'; - Axios.defaults.xsrfCookieName = 'csrftoken'; - const workerAxios = new WorkerWrappedAxios(); - Axios.interceptors.request.use((reqConfig) => { - if ('params' in reqConfig && 'org' in reqConfig.params) { - return reqConfig; - } - reqConfig.params = { ...enableOrganization(), ...(reqConfig.params || {}) }; - return reqConfig; - }); + async function share(directoryArg) { + const { backendAPI } = config; + const directory = encodeURI(directoryArg); - let token = store.get('token'); - if (token) { - Axios.defaults.headers.common.Authorization = `Token ${token}`; + let response = null; + try { + response = await Axios.get(`${backendAPI}/server/share`, { + proxy: config.proxy, + params: { directory }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function about() { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/server/about`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function exception(exceptionObject) { + const { backendAPI } = config; - return response.data; + try { + await Axios.post(`${backendAPI}/server/exception`, JSON.stringify(exceptionObject), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function share(directoryArg) { - const { backendAPI } = config; - const directory = encodeURI(directoryArg); - - let response = null; - try { - response = await Axios.get(`${backendAPI}/server/share`, { - proxy: config.proxy, - params: { directory }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function formats() { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.get(`${backendAPI}/server/annotation/formats`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function exception(exceptionObject) { - const { backendAPI } = config; + return response.data; + } - try { - await Axios.post(`${backendAPI}/server/exception`, JSON.stringify(exceptionObject), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function userAgreements() { + const { backendAPI } = config; + let response = null; + try { + response = await Axios.get(`${backendAPI}/restrictions/user-agreements`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function formats() { - const { backendAPI } = config; - - let response = null; - try { - response = await Axios.get(`${backendAPI}/server/annotation/formats`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + return response.data; + } - return response.data; + async function register(username, firstName, lastName, email, password1, password2, confirmations) { + let response = null; + try { + const data = JSON.stringify({ + username, + first_name: firstName, + last_name: lastName, + email, + password1, + password2, + confirmations, + }); + response = await Axios.post(`${config.backendAPI}/auth/register`, data, { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function userAgreements() { - const { backendAPI } = config; - let response = null; - try { - response = await Axios.get(`${backendAPI}/restrictions/user-agreements`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + return response.data; + } - return response.data; + async function login(username, password) { + const authenticationData = [ + `${encodeURIComponent('username')}=${encodeURIComponent(username)}`, + `${encodeURIComponent('password')}=${encodeURIComponent(password)}`, + ] + .join('&') + .replace(/%20/g, '+'); + + removeToken(); + let authenticationResponse = null; + try { + authenticationResponse = await Axios.post(`${config.backendAPI}/auth/login`, authenticationData, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function register(username, firstName, lastName, email, password1, password2, confirmations) { - let response = null; - try { - const data = JSON.stringify({ - username, - first_name: firstName, - last_name: lastName, - email, - password1, - password2, - confirmations, - }); - response = await Axios.post(`${config.backendAPI}/auth/register`, data, { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } - - return response.data; + if (authenticationResponse.headers['set-cookie']) { + // Browser itself setup cookie and header is none + // In NodeJS we need do it manually + const cookies = authenticationResponse.headers['set-cookie'].join(';'); + Axios.defaults.headers.common.Cookie = cookies; } - async function login(username, password) { - const authenticationData = [ - `${encodeURIComponent('username')}=${encodeURIComponent(username)}`, - `${encodeURIComponent('password')}=${encodeURIComponent(password)}`, - ] - .join('&') - .replace(/%20/g, '+'); + token = authenticationResponse.data.key; + store.set('token', token); + Axios.defaults.headers.common.Authorization = `Token ${token}`; + } + async function logout() { + try { + await Axios.post(`${config.backendAPI}/auth/logout`, { + proxy: config.proxy, + }); removeToken(); - let authenticationResponse = null; - try { - authenticationResponse = await Axios.post(`${config.backendAPI}/auth/login`, authenticationData, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } - - if (authenticationResponse.headers['set-cookie']) { - // Browser itself setup cookie and header is none - // In NodeJS we need do it manually - const cookies = authenticationResponse.headers['set-cookie'].join(';'); - Axios.defaults.headers.common.Cookie = cookies; - } - - token = authenticationResponse.data.key; - store.set('token', token); - Axios.defaults.headers.common.Authorization = `Token ${token}`; + } catch (errorData) { + throw generateError(errorData); } + } - async function logout() { - try { - await Axios.post(`${config.backendAPI}/auth/logout`, { - proxy: config.proxy, - }); - removeToken(); - } catch (errorData) { - throw generateError(errorData); - } + async function changePassword(oldPassword, newPassword1, newPassword2) { + try { + const data = JSON.stringify({ + old_password: oldPassword, + new_password1: newPassword1, + new_password2: newPassword2, + }); + await Axios.post(`${config.backendAPI}/auth/password/change`, data, { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function changePassword(oldPassword, newPassword1, newPassword2) { - try { - const data = JSON.stringify({ - old_password: oldPassword, - new_password1: newPassword1, - new_password2: newPassword2, - }); - await Axios.post(`${config.backendAPI}/auth/password/change`, data, { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function requestPasswordReset(email) { + try { + const data = JSON.stringify({ + email, + }); + await Axios.post(`${config.backendAPI}/auth/password/reset`, data, { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function requestPasswordReset(email) { - try { - const data = JSON.stringify({ - email, - }); - await Axios.post(`${config.backendAPI}/auth/password/reset`, data, { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function resetPassword(newPassword1, newPassword2, uid, _token) { + try { + const data = JSON.stringify({ + new_password1: newPassword1, + new_password2: newPassword2, + uid, + token: _token, + }); + await Axios.post(`${config.backendAPI}/auth/password/reset/confirm`, data, { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function resetPassword(newPassword1, newPassword2, uid, _token) { - try { - const data = JSON.stringify({ - new_password1: newPassword1, - new_password2: newPassword2, - uid, - token: _token, - }); - await Axios.post(`${config.backendAPI}/auth/password/reset/confirm`, data, { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); + async function authorized() { + try { + await getSelf(); + } catch (serverError) { + if (serverError.code === 401) { + removeToken(); + return false; } - } - async function authorized() { - try { - await module.exports.users.self(); - } catch (serverError) { - if (serverError.code === 401) { - removeToken(); - return false; - } + throw serverError; + } - throw serverError; - } + return true; + } - return true; + async function serverRequest(url, data) { + try { + return ( + await Axios({ + url, + ...data, + }) + ).data; + } catch (errorData) { + throw generateError(errorData); } + } - async function serverRequest(url, data) { - try { - return ( - await Axios({ - url, - ...data, - }) - ).data; - } catch (errorData) { - throw generateError(errorData); - } + async function searchProjectNames(search, limit) { + const { backendAPI, proxy } = config; + + let response = null; + try { + response = await Axios.get(`${backendAPI}/projects`, { + proxy, + params: { + names_only: true, + page: 1, + page_size: limit, + search, + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function searchProjectNames(search, limit) { - const { backendAPI, proxy } = config; + response.data.results.count = response.data.count; + return response.data.results; + } + + async function getProjects(filter = {}) { + const { backendAPI, proxy } = config; - let response = null; - try { - response = await Axios.get(`${backendAPI}/projects`, { + let response = null; + try { + if ('id' in filter) { + response = await Axios.get(`${backendAPI}/projects/${filter.id}`, { proxy, - params: { - names_only: true, - page: 1, - page_size: limit, - search, - }, }); - } catch (errorData) { - throw generateError(errorData); + const results = [response.data]; + results.count = 1; + return results; } - response.data.results.count = response.data.count; - return response.data.results; + response = await Axios.get(`${backendAPI}/projects`, { + params: { + ...filter, + page_size: 12, + }, + proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getProjects(filter = {}) { - const { backendAPI, proxy } = config; - - let response = null; - try { - if ('id' in filter) { - response = await Axios.get(`${backendAPI}/projects/${filter.id}`, { - proxy, - }); - const results = [response.data]; - results.count = 1; - return results; - } + response.data.results.count = response.data.count; + return response.data.results; + } - response = await Axios.get(`${backendAPI}/projects`, { - params: { - ...filter, - page_size: 12, - }, - proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function saveProject(id, projectData) { + const { backendAPI } = config; - response.data.results.count = response.data.count; - return response.data.results; + try { + await Axios.patch(`${backendAPI}/projects/${id}`, JSON.stringify(projectData), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function saveProject(id, projectData) { - const { backendAPI } = config; + async function deleteProject(id) { + const { backendAPI } = config; - try { - await Axios.patch(`${backendAPI}/projects/${id}`, JSON.stringify(projectData), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + try { + await Axios.delete(`${backendAPI}/projects/${id}`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function deleteProject(id) { - const { backendAPI } = config; + async function createProject(projectSpec) { + const { backendAPI } = config; - try { - await Axios.delete(`${backendAPI}/projects/${id}`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + try { + const response = await Axios.post(`${backendAPI}/projects`, JSON.stringify(projectSpec), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - async function createProject(projectSpec) { - const { backendAPI } = config; + async function getTasks(filter = {}) { + const { backendAPI } = config; - try { - const response = await Axios.post(`${backendAPI}/projects`, JSON.stringify(projectSpec), { + let response = null; + try { + if ('id' in filter) { + response = await Axios.get(`${backendAPI}/tasks/${filter.id}`, { proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, }); - return response.data; - } catch (errorData) { - throw generateError(errorData); + const results = [response.data]; + results.count = 1; + return results; } + + response = await Axios.get(`${backendAPI}/tasks`, { + params: { + ...filter, + page_size: 10, + }, + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getTasks(filter = {}) { - const { backendAPI } = config; - - let response = null; - try { - if ('id' in filter) { - response = await Axios.get(`${backendAPI}/tasks/${filter.id}`, { - proxy: config.proxy, - }); - const results = [response.data]; - results.count = 1; - return results; - } + response.data.results.count = response.data.count; + return response.data.results; + } - response = await Axios.get(`${backendAPI}/tasks`, { - params: { - ...filter, - page_size: 10, - }, - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function saveTask(id, taskData) { + const { backendAPI } = config; - response.data.results.count = response.data.count; - return response.data.results; + let response = null; + try { + response = await Axios.patch(`${backendAPI}/tasks/${id}`, JSON.stringify(taskData), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function saveTask(id, taskData) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.patch(`${backendAPI}/tasks/${id}`, JSON.stringify(taskData), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function deleteTask(id, organizationID = null) { + const { backendAPI } = config; - return response.data; + try { + await Axios.delete(`${backendAPI}/tasks/${id}`, { + ...(organizationID ? { org: organizationID } : {}), + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function deleteTask(id, organizationID = null) { + function exportDataset(instanceType) { + return async function ( + id: number, + format: string, + saveImages: boolean, + useDefaultSettings: boolean, + targetStorage: Storage, + name?: string, + ) { const { backendAPI } = config; + const baseURL = `${backendAPI}/${instanceType}/${id}/${saveImages ? 'dataset' : 'annotations'}`; + const params: Params = { + ...enableOrganization(), + ...configureStorage(targetStorage, useDefaultSettings), + ...(name ? { filename: name.replace(/\//g, '_') } : {}), + format, + }; - try { - await Axios.delete(`${backendAPI}/tasks/${id}`, { - ...(organizationID ? { org: organizationID } : {}), - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } - } + return new Promise((resolve, reject) => { + async function request() { + Axios.get(baseURL, { + proxy: config.proxy, + params, + }) + .then((response) => { + const isCloudStorage = targetStorage.location === StorageLocation.CLOUD_STORAGE; + const { status } = response; + if (status === 201) params.action = 'download'; + if (status === 202 || (isCloudStorage && status === 201)) { + setTimeout(request, 3000); + } else if (status === 201) { + resolve(`${baseURL}?${new URLSearchParams(params).toString()}`); + } else if (isCloudStorage && status === 200) { + resolve(); + } + }) + .catch((errorData) => { + reject(generateError(errorData)); + }); + } - function exportDataset(instanceType) { - return async function (id, format, name, saveImages) { - const { backendAPI } = config; - const baseURL = `${backendAPI}/${instanceType}/${id}/${saveImages ? 'dataset' : 'annotations'}`; - const params = { - ...enableOrganization(), - format, - }; + setTimeout(request); + }); + }; + } - if (name) { - params.filename = name.replace(/\//g, '_'); - } + async function importDataset( + id: number, + format: string, + useDefaultLocation: boolean, + sourceStorage: Storage, + file: File | string, + onUpdate, + ) { + const { backendAPI, origin } = config; + const params: Params = { + ...enableOrganization(), + ...configureStorage(sourceStorage, useDefaultLocation), + format, + filename: typeof file === 'string' ? file : file.name, + }; - return new Promise((resolve, reject) => { - async function request() { - Axios.get(baseURL, { + const url = `${backendAPI}/projects/${id}/dataset`; + + async function wait() { + return new Promise((resolve, reject) => { + async function requestStatus() { + try { + const response = await Axios.get(url, { + params: { ...params, action: 'import_status' }, proxy: config.proxy, - params, - }) - .then((response) => { - if (response.status === 202) { - setTimeout(request, 3000); - } else { - params.action = 'download'; - resolve(`${baseURL}?${new URLSearchParams(params).toString()}`); - } - }) - .catch((errorData) => { - reject(generateError(errorData)); - }); + }); + if (response.status === 202) { + if (onUpdate && response.data.message) { + onUpdate(response.data.message, response.data.progress || 0); + } + setTimeout(requestStatus, 3000); + } else if (response.status === 201) { + resolve(); + } else { + reject(generateError(response)); + } + } catch (error) { + reject(generateError(error)); } - - setTimeout(request); - }); - }; + } + setTimeout(requestStatus, 2000); + }); } + const isCloudStorage = sourceStorage.location === StorageLocation.CLOUD_STORAGE; - async function importDataset(id, format, file, onUpdate) { - const { backendAPI, origin } = config; - const params = { - ...enableOrganization(), - format, - filename: file.name, - }; + if (isCloudStorage) { + try { + await Axios.post(url, + new FormData(), { + params, + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); + } + } else { const uploadConfig = { chunkSize: config.uploadChunkSize * 1024 * 1024, endpoint: `${origin}${backendAPI}/projects/${id}/dataset/`, totalSentSize: 0, - totalSize: file.size, + totalSize: (file as File).size, onUpdate: (percentage) => { onUpdate('The dataset is being uploaded to the server', percentage); }, }; - const url = `${backendAPI}/projects/${id}/dataset`; try { await Axios.post(url, @@ -649,77 +732,103 @@ proxy: config.proxy, headers: { 'Upload-Finish': true }, }); - return new Promise((resolve, reject) => { - async function requestStatus() { - try { - const response = await Axios.get(url, { - params: { ...params, action: 'import_status' }, - proxy: config.proxy, - }); - if (response.status === 202) { - if (onUpdate && response.data.message) { - onUpdate(response.data.message, response.data.progress || 0); - } - setTimeout(requestStatus, 3000); - } else if (response.status === 201) { - resolve(); - } else { - reject(generateError(response)); - } - } catch (error) { - reject(generateError(error)); - } - } - setTimeout(requestStatus, 2000); - }); } catch (errorData) { throw generateError(errorData); } } + try { + return wait(); + } catch (errorData) { + throw generateError(errorData); + } + } - async function exportTask(id) { - const { backendAPI } = config; - const params = { - ...enableOrganization(), - }; - const url = `${backendAPI}/tasks/${id}/backup`; + async function backupTask(id: number, targetStorage: Storage, useDefaultSettings: boolean, fileName?: string) { + const { backendAPI } = config; + const params: Params = { + ...enableOrganization(), + ...configureStorage(targetStorage, useDefaultSettings), + ...(fileName ? { filename: fileName } : {}), + }; + const url = `${backendAPI}/tasks/${id}/backup`; + + return new Promise((resolve, reject) => { + async function request() { + try { + const response = await Axios.get(url, { + proxy: config.proxy, + params, + }); + const isCloudStorage = targetStorage.location === StorageLocation.CLOUD_STORAGE; + const { status } = response; + if (status === 201) params.action = 'download'; + if (status === 202 || (isCloudStorage && status === 201)) { + setTimeout(request, 3000); + } else if (status === 201) { + resolve(`${url}?${new URLSearchParams(params).toString()}`); + } else if (isCloudStorage && status === 200) { + resolve(); + } + } catch (errorData) { + reject(generateError(errorData)); + } + } + + setTimeout(request); + }); + } + + async function restoreTask(storage: Storage, file: File | string) { + const { backendAPI } = config; + // keep current default params to 'freeze" them during this request + const params: Params = { + ...enableOrganization(), + ...configureStorage(storage), + }; + const url = `${backendAPI}/tasks/backup`; + const taskData = new FormData(); + let response; + + async function wait() { return new Promise((resolve, reject) => { - async function request() { + async function checkStatus() { try { - const response = await Axios.get(url, { + taskData.set('rq_id', response.data.rq_id); + response = await Axios.post(url, taskData, { proxy: config.proxy, params, }); if (response.status === 202) { - setTimeout(request, 3000); + setTimeout(checkStatus, 3000); } else { - params.action = 'download'; - resolve(`${url}?${new URLSearchParams(params).toString()}`); + // to be able to get the task after it was created, pass frozen params + const importedTask = await getTasks({ id: response.data.id, ...params }); + resolve(importedTask[0]); } } catch (errorData) { reject(generateError(errorData)); } } - - setTimeout(request); + setTimeout(checkStatus); }); } + const isCloudStorage = storage.location === StorageLocation.CLOUD_STORAGE; - async function importTask(file) { - const { backendAPI } = config; - // keep current default params to 'freeze" them during this request - const params = enableOrganization(); - - const taskData = new FormData(); + if (isCloudStorage) { + params.filename = file as string; + response = await Axios.post(url, + new FormData(), { + params, + proxy: config.proxy, + }); + } else { const uploadConfig = { chunkSize: config.uploadChunkSize * 1024 * 1024, endpoint: `${origin}${backendAPI}/tasks/backup/`, totalSentSize: 0, - totalSize: file.size, + totalSize: (file as File).size, }; - - const url = `${backendAPI}/tasks/backup`; await Axios.post(url, new FormData(), { params, @@ -727,55 +836,85 @@ headers: { 'Upload-Start': true }, }); const { filename } = await chunkUpload(file, uploadConfig); - let response = await Axios.post(url, + response = await Axios.post(url, new FormData(), { params: { ...params, filename }, proxy: config.proxy, headers: { 'Upload-Finish': true }, }); + } + return wait(); + } - return new Promise((resolve, reject) => { - async function checkStatus() { - try { - taskData.set('rq_id', response.data.rq_id); - response = await Axios.post(url, taskData, { - proxy: config.proxy, - params, - }); - if (response.status === 202) { - setTimeout(checkStatus, 3000); - } else { - // to be able to get the task after it was created, pass frozen params - const importedTask = await getTasks({ id: response.data.id, ...params }); - resolve(importedTask[0]); - } - } catch (errorData) { - reject(generateError(errorData)); + async function backupProject( + id: number, + targetStorage: Storage, + useDefaultSettings: boolean, + fileName?: string, + ) { + const { backendAPI } = config; + // keep current default params to 'freeze" them during this request + const params: Params = { + ...enableOrganization(), + ...configureStorage(targetStorage, useDefaultSettings), + ...(fileName ? { filename: fileName } : {}), + }; + + const url = `${backendAPI}/projects/${id}/backup`; + + return new Promise((resolve, reject) => { + async function request() { + try { + const response = await Axios.get(url, { + proxy: config.proxy, + params, + }); + const isCloudStorage = targetStorage.location === StorageLocation.CLOUD_STORAGE; + const { status } = response; + if (status === 201) params.action = 'download'; + if (status === 202 || (isCloudStorage && status === 201)) { + setTimeout(request, 3000); + } else if (status === 201) { + resolve(`${url}?${new URLSearchParams(params).toString()}`); + } else if (isCloudStorage && status === 200) { + resolve(); } + } catch (errorData) { + reject(generateError(errorData)); } + } - setTimeout(checkStatus); - }); - } + setTimeout(request); + }); + } - async function backupProject(id) { - const { backendAPI } = config; - // keep current default params to 'freeze" them during this request - const params = enableOrganization(); - const url = `${backendAPI}/projects/${id}/backup`; + async function restoreProject(storage: Storage, file: File | string) { + const { backendAPI } = config; + // keep current default params to 'freeze" them during this request + const params: Params = { + ...enableOrganization(), + ...configureStorage(storage), + }; + + const url = `${backendAPI}/projects/backup`; + const projectData = new FormData(); + let response; + async function wait() { return new Promise((resolve, reject) => { async function request() { try { - const response = await Axios.get(url, { + projectData.set('rq_id', response.data.rq_id); + response = await Axios.post(`${backendAPI}/projects/backup`, projectData, { proxy: config.proxy, params, }); if (response.status === 202) { setTimeout(request, 3000); } else { - params.action = 'download'; - resolve(`${url}?${new URLSearchParams(params).toString()}`); + // to be able to get the task after it was created, pass frozen params + const restoredProject = await getProjects({ id: response.data.id, ...params }); + resolve(restoredProject[0]); } } catch (errorData) { reject(generateError(errorData)); @@ -783,23 +922,25 @@ } setTimeout(request); - }); - } + }) + }; - async function restoreProject(file) { - const { backendAPI } = config; - // keep current default params to 'freeze" them during this request - const params = enableOrganization(); + const isCloudStorage = storage.location === StorageLocation.CLOUD_STORAGE; - const projectData = new FormData(); + if (isCloudStorage) { + params.filename = file; + response = await Axios.post(url, + new FormData(), { + params, + proxy: config.proxy, + }); + } else { const uploadConfig = { chunkSize: config.uploadChunkSize * 1024 * 1024, endpoint: `${origin}${backendAPI}/projects/backup/`, totalSentSize: 0, - totalSize: file.size, + totalSize: (file as File).size, }; - - const url = `${backendAPI}/projects/backup`; await Axios.post(url, new FormData(), { params, @@ -807,1239 +948,1252 @@ headers: { 'Upload-Start': true }, }); const { filename } = await chunkUpload(file, uploadConfig); - let response = await Axios.post(url, + response = await Axios.post(url, new FormData(), { params: { ...params, filename }, proxy: config.proxy, headers: { 'Upload-Finish': true }, }); + } + return wait(); + } + + async function createTask(taskSpec, taskDataSpec, onUpdate) { + const { backendAPI, origin } = config; + // keep current default params to 'freeze" them during this request + const params = enableOrganization(); + async function wait(id) { return new Promise((resolve, reject) => { - async function request() { + async function checkStatus() { try { - projectData.set('rq_id', response.data.rq_id); - response = await Axios.post(`${backendAPI}/projects/backup`, projectData, { - proxy: config.proxy, - params, - }); - if (response.status === 202) { - setTimeout(request, 3000); + const response = await Axios.get(`${backendAPI}/tasks/${id}/status`, { params }); + if (['Queued', 'Started'].includes(response.data.state)) { + if (response.data.message !== '') { + onUpdate(response.data.message, response.data.progress || 0); + } + setTimeout(checkStatus, 1000); + } else if (response.data.state === 'Finished') { + resolve(); + } else if (response.data.state === 'Failed') { + // If request has been successful, but task hasn't been created + // Then passed data is wrong and we can pass code 400 + const message = ` + Could not create the task on the server. ${response.data.message}. + `; + reject(new ServerError(message, 400)); } else { - // to be able to get the task after it was created, pass frozen params - const restoredProject = await getProjects({ id: response.data.id, ...params }); - resolve(restoredProject[0]); + // If server has another status, it is unexpected + // Therefore it is server error and we can pass code 500 + reject( + new ServerError( + `Unknown task state has been received: ${response.data.state}`, + 500, + ), + ); } } catch (errorData) { reject(generateError(errorData)); } } - setTimeout(request); + setTimeout(checkStatus, 1000); }); } - async function createTask(taskSpec, taskDataSpec, onUpdate) { - const { backendAPI, origin } = config; - // keep current default params to 'freeze" them during this request - const params = enableOrganization(); - - async function wait(id) { - return new Promise((resolve, reject) => { - async function checkStatus() { - try { - const response = await Axios.get(`${backendAPI}/tasks/${id}/status`, { params }); - if (['Queued', 'Started'].includes(response.data.state)) { - if (response.data.message !== '') { - onUpdate(response.data.message, response.data.progress || 0); - } - setTimeout(checkStatus, 1000); - } else if (response.data.state === 'Finished') { - resolve(); - } else if (response.data.state === 'Failed') { - // If request has been successful, but task hasn't been created - // Then passed data is wrong and we can pass code 400 - const message = ` - Could not create the task on the server. ${response.data.message}. - `; - reject(new ServerError(message, 400)); - } else { - // If server has another status, it is unexpected - // Therefore it is server error and we can pass code 500 - reject( - new ServerError( - `Unknown task state has been received: ${response.data.state}`, - 500, - ), - ); - } - } catch (errorData) { - reject(generateError(errorData)); - } - } + const chunkSize = config.uploadChunkSize * 1024 * 1024; + const clientFiles = taskDataSpec.client_files; + const chunkFiles = []; + const bulkFiles = []; + let totalSize = 0; + let totalSentSize = 0; + for (const file of clientFiles) { + if (file.size > chunkSize) { + chunkFiles.push(file); + } else { + bulkFiles.push(file); + } + totalSize += file.size; + } + delete taskDataSpec.client_files; - setTimeout(checkStatus, 1000); + const taskData = new FormData(); + for (const [key, value] of Object.entries(taskDataSpec)) { + if (Array.isArray(value)) { + value.forEach((element, idx) => { + taskData.append(`${key}[${idx}]`, element); }); + } else { + taskData.set(key, value); } + } - const chunkSize = config.uploadChunkSize * 1024 * 1024; - const clientFiles = taskDataSpec.client_files; - const chunkFiles = []; - const bulkFiles = []; - let totalSize = 0; - let totalSentSize = 0; - for (const file of clientFiles) { - if (file.size > chunkSize) { - chunkFiles.push(file); - } else { - bulkFiles.push(file); - } - totalSize += file.size; - } - delete taskDataSpec.client_files; + let response = null; - const taskData = new FormData(); - for (const [key, value] of Object.entries(taskDataSpec)) { - if (Array.isArray(value)) { - value.forEach((element, idx) => { - taskData.append(`${key}[${idx}]`, element); - }); + onUpdate('The task is being created on the server..', null); + try { + response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskSpec), { + proxy: config.proxy, + params, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); + } + + onUpdate('The data are being uploaded to the server..', null); + + async function bulkUpload(taskId, files) { + const fileBulks = files.reduce((fileGroups, file) => { + const lastBulk = fileGroups[fileGroups.length - 1]; + if (chunkSize - lastBulk.size >= file.size) { + lastBulk.files.push(file); + lastBulk.size += file.size; } else { - taskData.set(key, value); + fileGroups.push({ files: [file], size: file.size }); + } + return fileGroups; + }, [{ files: [], size: 0 }]); + const totalBulks = fileBulks.length; + let currentChunkNumber = 0; + while (currentChunkNumber < totalBulks) { + for (const [idx, element] of fileBulks[currentChunkNumber].files.entries()) { + taskData.append(`client_files[${idx}]`, element); + } + const percentage = totalSentSize / totalSize; + onUpdate('The data are being uploaded to the server', percentage); + await Axios.post(`${backendAPI}/tasks/${taskId}/data`, taskData, { + ...params, + proxy: config.proxy, + headers: { 'Upload-Multiple': true }, + }); + for (let i = 0; i < fileBulks[currentChunkNumber].files.length; i++) { + taskData.delete(`client_files[${i}]`); } + totalSentSize += fileBulks[currentChunkNumber].size; + currentChunkNumber++; } + } - let response = null; - - onUpdate('The task is being created on the server..', null); - try { - response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskSpec), { + try { + await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, + taskData, { + ...params, proxy: config.proxy, - params, - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Upload-Start': true }, }); - } catch (errorData) { - throw generateError(errorData); + const uploadConfig = { + endpoint: `${origin}${backendAPI}/tasks/${response.data.id}/data/`, + onUpdate: (percentage) => { + onUpdate('The data are being uploaded to the server', percentage); + }, + chunkSize, + totalSize, + totalSentSize, + }; + for (const file of chunkFiles) { + uploadConfig.totalSentSize += await chunkUpload(file, uploadConfig); } + if (bulkFiles.length > 0) { + await bulkUpload(response.data.id, bulkFiles); + } + await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, + taskData, { + ...params, + proxy: config.proxy, + headers: { 'Upload-Finish': true }, + }); + } catch (errorData) { + try { + await deleteTask(response.data.id, params.org || null); + } catch (_) { + // ignore + } + throw generateError(errorData); + } - onUpdate('The data are being uploaded to the server..', null); + try { + await wait(response.data.id); + } catch (createException) { + await deleteTask(response.data.id, params.org || null); + throw createException; + } - async function bulkUpload(taskId, files) { - const fileBulks = files.reduce((fileGroups, file) => { - const lastBulk = fileGroups[fileGroups.length - 1]; - if (chunkSize - lastBulk.size >= file.size) { - lastBulk.files.push(file); - lastBulk.size += file.size; - } else { - fileGroups.push({ files: [file], size: file.size }); - } - return fileGroups; - }, [{ files: [], size: 0 }]); - const totalBulks = fileBulks.length; - let currentChunkNumber = 0; - while (currentChunkNumber < totalBulks) { - for (const [idx, element] of fileBulks[currentChunkNumber].files.entries()) { - taskData.append(`client_files[${idx}]`, element); - } - const percentage = totalSentSize / totalSize; - onUpdate('The data are being uploaded to the server', percentage); - await Axios.post(`${backendAPI}/tasks/${taskId}/data`, taskData, { - ...params, - proxy: config.proxy, - headers: { 'Upload-Multiple': true }, - }); - for (let i = 0; i < fileBulks[currentChunkNumber].files.length; i++) { - taskData.delete(`client_files[${i}]`); - } - totalSentSize += fileBulks[currentChunkNumber].size; - currentChunkNumber++; - } - } + // to be able to get the task after it was created, pass frozen params + const createdTask = await getTasks({ id: response.data.id, ...params }); + return createdTask[0]; + } - try { - await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, - taskData, { - ...params, - proxy: config.proxy, - headers: { 'Upload-Start': true }, - }); - const uploadConfig = { - endpoint: `${origin}${backendAPI}/tasks/${response.data.id}/data/`, - onUpdate: (percentage) => { - onUpdate('The data are being uploaded to the server', percentage); - }, - chunkSize, - totalSize, - totalSentSize, - }; - for (const file of chunkFiles) { - uploadConfig.totalSentSize += await chunkUpload(file, uploadConfig); - } - if (bulkFiles.length > 0) { - await bulkUpload(response.data.id, bulkFiles); - } - await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, - taskData, { - ...params, - proxy: config.proxy, - headers: { 'Upload-Finish': true }, - }); - } catch (errorData) { - try { - await deleteTask(response.data.id, params.org || null); - } catch (_) { - // ignore - } - throw generateError(errorData); - } + async function getJobs(filter = {}) { + const { backendAPI } = config; + const id = filter.id || null; - try { - await wait(response.data.id); - } catch (createException) { - await deleteTask(response.data.id, params.org || null); - throw createException; + let response = null; + try { + if (id !== null) { + response = await Axios.get(`${backendAPI}/jobs/${id}`, { + proxy: config.proxy, + }); + } else { + response = await Axios.get(`${backendAPI}/jobs`, { + proxy: config.proxy, + params: { + ...filter, + page_size: 12, + }, + }); } - - // to be able to get the task after it was created, pass frozen params - const createdTask = await getTasks({ id: response.data.id, ...params }); - return createdTask[0]; + } catch (errorData) { + throw generateError(errorData); } - async function getJobs(filter = {}) { - const { backendAPI } = config; - const id = filter.id || null; + return response.data; + } - let response = null; - try { - if (id !== null) { - response = await Axios.get(`${backendAPI}/jobs/${id}`, { - proxy: config.proxy, - }); - } else { - response = await Axios.get(`${backendAPI}/jobs`, { - proxy: config.proxy, - params: { - ...filter, - page_size: 12, - }, - }); - } - } catch (errorData) { - throw generateError(errorData); - } + async function getJobIssues(jobID) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.get(`${backendAPI}/jobs/${jobID}/issues`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getJobIssues(jobID) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/jobs/${jobID}/issues`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function createComment(data) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.post(`${backendAPI}/comments`, JSON.stringify(data), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function createComment(data) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.post(`${backendAPI}/comments`, JSON.stringify(data), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function createIssue(data) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.post(`${backendAPI}/issues`, JSON.stringify(data), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function createIssue(data) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.post(`${backendAPI}/issues`, JSON.stringify(data), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function updateIssue(issueID, data) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.patch(`${backendAPI}/issues/${issueID}`, JSON.stringify(data), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function updateIssue(issueID, data) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.patch(`${backendAPI}/issues/${issueID}`, JSON.stringify(data), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function deleteIssue(issueID) { + const { backendAPI } = config; - return response.data; + try { + await Axios.delete(`${backendAPI}/issues/${issueID}`); + } catch (errorData) { + throw generateError(errorData); } + } - async function deleteIssue(issueID) { - const { backendAPI } = config; + async function saveJob(id, jobData) { + const { backendAPI } = config; - try { - await Axios.delete(`${backendAPI}/issues/${issueID}`); - } catch (errorData) { - throw generateError(errorData); - } + let response = null; + try { + response = await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function saveJob(id, jobData) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function getUsers(filter = { page_size: 'all' }) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.get(`${backendAPI}/users`, { + proxy: config.proxy, + params: { + ...filter, + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getUsers(filter = { page_size: 'all' }) { - const { backendAPI } = config; + return response.data.results; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/users`, { - proxy: config.proxy, - params: { - ...filter, - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function getSelf() { + const { backendAPI } = config; - return response.data.results; + let response = null; + try { + response = await Axios.get(`${backendAPI}/users/self`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getSelf() { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/users/self`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function getPreview(tid, jid) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + const url = `${backendAPI}/${jid !== null ? 'jobs' : 'tasks'}/${jid || tid}/data`; + response = await Axios.get(url, { + params: { + type: 'preview', + }, + proxy: config.proxy, + responseType: 'blob', + }); + } catch (errorData) { + const code = errorData.response ? errorData.response.status : errorData.code; + throw new ServerError(`Could not get preview frame for the task ${tid} from the server`, code); } - async function getPreview(tid, jid) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - const url = `${backendAPI}/${jid !== null ? 'jobs' : 'tasks'}/${jid || tid}/data`; - response = await Axios.get(url, { - params: { - type: 'preview', - }, - proxy: config.proxy, - responseType: 'blob', - }); - } catch (errorData) { - const code = errorData.response ? errorData.response.status : errorData.code; - throw new ServerError(`Could not get preview frame for the task ${tid} from the server`, code); - } + async function getImageContext(jid, frame) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.get(`${backendAPI}/jobs/${jid}/data`, { + params: { + quality: 'original', + type: 'context_image', + number: frame, + }, + proxy: config.proxy, + responseType: 'blob', + }); + } catch (errorData) { + throw generateError(errorData); } - async function getImageContext(jid, frame) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/jobs/${jid}/data`, { - params: { - quality: 'original', - type: 'context_image', - number: frame, - }, - proxy: config.proxy, - responseType: 'blob', - }); - } catch (errorData) { - throw generateError(errorData); - } + async function getData(tid, jid, chunk) { + const { backendAPI } = config; - return response.data; - } + const url = jid === null ? `tasks/${tid}/data` : `jobs/${jid}/data`; - async function getData(tid, jid, chunk) { - const { backendAPI } = config; + let response = null; + try { + response = await workerAxios.get(`${backendAPI}/${url}`, { + params: { + ...enableOrganization(), + quality: 'compressed', + type: 'chunk', + number: chunk, + }, + proxy: config.proxy, + responseType: 'arraybuffer', + }); + } catch (errorData) { + throw generateError({ + message: '', + response: { + ...errorData.response, + data: String.fromCharCode.apply(null, new Uint8Array(errorData.response.data)), + }, + }); + } - const url = jid === null ? `tasks/${tid}/data` : `jobs/${jid}/data`; + return response; + } - let response = null; - try { - response = await workerAxios.get(`${backendAPI}/${url}`, { - params: { - ...enableOrganization(), - quality: 'compressed', - type: 'chunk', - number: chunk, - }, - proxy: config.proxy, - responseType: 'arraybuffer', - }); - } catch (errorData) { - throw generateError({ - message: '', - response: { - ...errorData.response, - data: String.fromCharCode.apply(null, new Uint8Array(errorData.response.data)), - }, - }); - } + async function getMeta(session, jid) { + const { backendAPI } = config; - return response; + let response = null; + try { + response = await Axios.get(`${backendAPI}/${session}s/${jid}/data/meta`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getMeta(session, jid) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/${session}s/${jid}/data/meta`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function saveMeta(session, jid, meta) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.patch(`${backendAPI}/${session}s/${jid}/data/meta`, meta, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function saveMeta(session, jid, meta) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.patch(`${backendAPI}/${session}s/${jid}/data/meta`, meta, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + // Session is 'task' or 'job' + async function getAnnotations(session, id) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.get(`${backendAPI}/${session}s/${id}/annotations`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - // Session is 'task' or 'job' - async function getAnnotations(session, id) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/${session}s/${id}/annotations`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + // Session is 'task' or 'job' + async function updateAnnotations(session, id, data, action) { + const { backendAPI } = config; + const url = `${backendAPI}/${session}s/${id}/annotations`; + const params = {}; + let requestFunc = null; - return response.data; + if (action.toUpperCase() === 'PUT') { + requestFunc = Axios.put.bind(Axios); + } else { + requestFunc = Axios.patch.bind(Axios); + params.action = action; } - // Session is 'task' or 'job' - async function updateAnnotations(session, id, data, action) { - const { backendAPI } = config; - const url = `${backendAPI}/${session}s/${id}/annotations`; - const params = {}; - let requestFunc = null; + let response = null; + try { + response = await requestFunc(url, JSON.stringify(data), { + proxy: config.proxy, + params, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); + } - if (action.toUpperCase() === 'PUT') { - requestFunc = Axios.put.bind(Axios); - } else { - requestFunc = Axios.patch.bind(Axios); - params.action = action; - } + return response.data; + } - let response = null; + // Session is 'task' or 'job' + async function uploadAnnotations( + session, + id: number, + format: string, + useDefaultLocation: boolean, + sourceStorage: Storage, + file: File | string, + ) { + const { backendAPI, origin } = config; + const params: Params = { + ...enableOrganization(), + ...configureStorage(sourceStorage, useDefaultLocation), + format, + filename: typeof file === 'string' ? file : file.name, + }; + + const url = `${backendAPI}/${session}s/${id}/annotations`; + + async function wait() { + return new Promise((resolve, reject) => { + async function requestStatus() { + try { + const response = await Axios.put( + url, + new FormData(), + { + params, + proxy: config.proxy, + }, + ); + if (response.status === 202) { + setTimeout(requestStatus, 3000); + } else { + resolve(); + } + } catch (errorData) { + reject(generateError(errorData)); + } + } + setTimeout(requestStatus); + }); + } + const isCloudStorage = sourceStorage.location === StorageLocation.CLOUD_STORAGE; + + if (isCloudStorage) { try { - response = await requestFunc(url, JSON.stringify(data), { - proxy: config.proxy, - params, - headers: { - 'Content-Type': 'application/json', - }, - }); + await Axios.post(url, + new FormData(), { + params, + proxy: config.proxy, + }); } catch (errorData) { throw generateError(errorData); } - - return response.data; - } - - // Session is 'task' or 'job' - async function uploadAnnotations(session, id, file, format) { - const { backendAPI, origin } = config; - const params = { - ...enableOrganization(), - format, - filename: file.name, - }; + } else { const chunkSize = config.uploadChunkSize * 1024 * 1024; const uploadConfig = { chunkSize, endpoint: `${origin}${backendAPI}/${session}s/${id}/annotations/`, }; + try { - await Axios.post(`${backendAPI}/${session}s/${id}/annotations`, + await Axios.post(url, new FormData(), { params, proxy: config.proxy, headers: { 'Upload-Start': true }, }); await chunkUpload(file, uploadConfig); - await Axios.post(`${backendAPI}/${session}s/${id}/annotations`, + await Axios.post(url, new FormData(), { params, proxy: config.proxy, headers: { 'Upload-Finish': true }, }); - return new Promise((resolve, reject) => { - async function requestStatus() { - try { - const response = await Axios.put( - `${backendAPI}/${session}s/${id}/annotations`, - new FormData(), - { - params, - proxy: config.proxy, - }, - ); - if (response.status === 202) { - setTimeout(requestStatus, 3000); - } else { - resolve(); - } - } catch (errorData) { - reject(generateError(errorData)); - } - } - setTimeout(requestStatus); - }); } catch (errorData) { throw generateError(errorData); } } - // Session is 'task' or 'job' - async function dumpAnnotations(id, name, format) { - const { backendAPI } = config; - const baseURL = `${backendAPI}/tasks/${id}/annotations`; - const params = enableOrganization(); - params.format = encodeURIComponent(format); - if (name) { - const filename = name.replace(/\//g, '_'); - params.filename = encodeURIComponent(filename); - } - - return new Promise((resolve, reject) => { - async function request() { - Axios.get(baseURL, { - proxy: config.proxy, - params, - }) - .then((response) => { - if (response.status === 202) { - setTimeout(request, 3000); - } else { - params.action = 'download'; - resolve(`${baseURL}?${new URLSearchParams(params).toString()}`); - } - }) - .catch((errorData) => { - reject(generateError(errorData)); - }); - } - - setTimeout(request); - }); + try { + return wait(); + } catch (errorData) { + throw generateError(errorData); } + } - async function saveLogs(logs) { - const { backendAPI } = config; + // Session is 'task' or 'job' + async function dumpAnnotations(id, name, format) { + const { backendAPI } = config; + const baseURL = `${backendAPI}/tasks/${id}/annotations`; + const params = enableOrganization(); + params.format = encodeURIComponent(format); + if (name) { + const filename = name.replace(/\//g, '_'); + params.filename = encodeURIComponent(filename); + } - try { - await Axios.post(`${backendAPI}/server/logs`, JSON.stringify(logs), { + return new Promise((resolve, reject) => { + async function request() { + Axios.get(baseURL, { proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); + params, + }) + .then((response) => { + if (response.status === 202) { + setTimeout(request, 3000); + } else { + params.action = 'download'; + resolve(`${baseURL}?${new URLSearchParams(params).toString()}`); + } + }) + .catch((errorData) => { + reject(generateError(errorData)); + }); } + + setTimeout(request); + }); + } + + async function saveLogs(logs) { + const { backendAPI } = config; + + try { + await Axios.post(`${backendAPI}/server/logs`, JSON.stringify(logs), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function getLambdaFunctions() { - const { backendAPI } = config; + async function getLambdaFunctions() { + const { backendAPI } = config; - try { - const response = await Axios.get(`${backendAPI}/lambda/functions`, { - proxy: config.proxy, - }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } + try { + const response = await Axios.get(`${backendAPI}/lambda/functions`, { + proxy: config.proxy, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - async function runLambdaRequest(body) { - const { backendAPI } = config; + async function runLambdaRequest(body) { + const { backendAPI } = config; - try { - const response = await Axios.post(`${backendAPI}/lambda/requests`, JSON.stringify(body), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); + try { + const response = await Axios.post(`${backendAPI}/lambda/requests`, JSON.stringify(body), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - async function callLambdaFunction(funId, body) { - const { backendAPI } = config; + async function callLambdaFunction(funId, body) { + const { backendAPI } = config; - try { - const response = await Axios.post(`${backendAPI}/lambda/functions/${funId}`, JSON.stringify(body), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); + try { + const response = await Axios.post(`${backendAPI}/lambda/functions/${funId}`, JSON.stringify(body), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - async function getLambdaRequests() { - const { backendAPI } = config; + async function getLambdaRequests() { + const { backendAPI } = config; - try { - const response = await Axios.get(`${backendAPI}/lambda/requests`, { - proxy: config.proxy, - }); + try { + const response = await Axios.get(`${backendAPI}/lambda/requests`, { + proxy: config.proxy, + }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - async function getRequestStatus(requestID) { - const { backendAPI } = config; + async function getRequestStatus(requestID) { + const { backendAPI } = config; - try { - const response = await Axios.get(`${backendAPI}/lambda/requests/${requestID}`, { - proxy: config.proxy, - }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } + try { + const response = await Axios.get(`${backendAPI}/lambda/requests/${requestID}`, { + proxy: config.proxy, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - async function cancelLambdaRequest(requestId) { - const { backendAPI } = config; + async function cancelLambdaRequest(requestId) { + const { backendAPI } = config; - try { - await Axios.delete(`${backendAPI}/lambda/requests/${requestId}`, { - method: 'DELETE', - }); - } catch (errorData) { - throw generateError(errorData); - } + try { + await Axios.delete(`${backendAPI}/lambda/requests/${requestId}`, { + method: 'DELETE', + }); + } catch (errorData) { + throw generateError(errorData); } + } - function predictorStatus(projectId) { - const { backendAPI } = config; + function predictorStatus(projectId) { + const { backendAPI } = config; - return new Promise((resolve, reject) => { - async function request() { - try { - const response = await Axios.get(`${backendAPI}/predict/status`, { - params: { - project: projectId, - }, - }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } + return new Promise((resolve, reject) => { + async function request() { + try { + const response = await Axios.get(`${backendAPI}/predict/status`, { + params: { + project: projectId, + }, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - const timeoutCallback = async () => { - let data = null; - try { - data = await request(); - if (data.status === 'queued') { - setTimeout(timeoutCallback, 1000); - } else if (data.status === 'done') { - resolve(data); - } else { - throw new Error(`Unknown status was received "${data.status}"`); - } - } catch (error) { - reject(error); + const timeoutCallback = async () => { + let data = null; + try { + data = await request(); + if (data.status === 'queued') { + setTimeout(timeoutCallback, 1000); + } else if (data.status === 'done') { + resolve(data); + } else { + throw new Error(`Unknown status was received "${data.status}"`); } - }; + } catch (error) { + reject(error); + } + }; - setTimeout(timeoutCallback); - }); - } + setTimeout(timeoutCallback); + }); + } - function predictAnnotations(taskId, frame) { - return new Promise((resolve, reject) => { - const { backendAPI } = config; + function predictAnnotations(taskId, frame) { + return new Promise((resolve, reject) => { + const { backendAPI } = config; - async function request() { - try { - const response = await Axios.get(`${backendAPI}/predict/frame`, { - params: { - task: taskId, - frame, - }, - }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } + async function request() { + try { + const response = await Axios.get(`${backendAPI}/predict/frame`, { + params: { + task: taskId, + frame, + }, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - const timeoutCallback = async () => { - let data = null; - try { - data = await request(); - if (data.status === 'queued') { - setTimeout(timeoutCallback, 1000); - } else if (data.status === 'done') { - predictAnnotations.latestRequest.fetching = false; - resolve(data.annotation); - } else { - throw new Error(`Unknown status was received "${data.status}"`); - } - } catch (error) { + const timeoutCallback = async () => { + let data = null; + try { + data = await request(); + if (data.status === 'queued') { + setTimeout(timeoutCallback, 1000); + } else if (data.status === 'done') { predictAnnotations.latestRequest.fetching = false; - reject(error); + resolve(data.annotation); + } else { + throw new Error(`Unknown status was received "${data.status}"`); } - }; - - const closureId = Date.now(); - predictAnnotations.latestRequest.id = closureId; - const predicate = () => !predictAnnotations.latestRequest.fetching || - predictAnnotations.latestRequest.id !== closureId; - if (predictAnnotations.latestRequest.fetching) { - waitFor(5, predicate).then(() => { - if (predictAnnotations.latestRequest.id !== closureId) { - resolve(null); - } else { - predictAnnotations.latestRequest.fetching = true; - setTimeout(timeoutCallback); - } - }); - } else { - predictAnnotations.latestRequest.fetching = true; - setTimeout(timeoutCallback); + } catch (error) { + predictAnnotations.latestRequest.fetching = false; + reject(error); } + }; + + const closureId = Date.now(); + predictAnnotations.latestRequest.id = closureId; + const predicate = () => !predictAnnotations.latestRequest.fetching || + predictAnnotations.latestRequest.id !== closureId; + if (predictAnnotations.latestRequest.fetching) { + waitFor(5, predicate).then(() => { + if (predictAnnotations.latestRequest.id !== closureId) { + resolve(null); + } else { + predictAnnotations.latestRequest.fetching = true; + setTimeout(timeoutCallback); + } + }); + } else { + predictAnnotations.latestRequest.fetching = true; + setTimeout(timeoutCallback); + } + }); + } + + predictAnnotations.latestRequest = { + fetching: false, + id: null, + }; + + async function installedApps() { + const { backendAPI } = config; + try { + const response = await Axios.get(`${backendAPI}/server/plugins`, { + proxy: config.proxy, }); + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - predictAnnotations.latestRequest = { - fetching: false, - id: null, - }; + async function createCloudStorage(storageDetail) { + const { backendAPI } = config; - async function installedApps() { - const { backendAPI } = config; - try { - const response = await Axios.get(`${backendAPI}/server/plugins`, { - proxy: config.proxy, - }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } + const storageDetailData = prepareData(storageDetail); + try { + const response = await Axios.post(`${backendAPI}/cloudstorages`, storageDetailData, { + proxy: config.proxy, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); } + } - async function createCloudStorage(storageDetail) { - const { backendAPI } = config; + async function updateCloudStorage(id, storageDetail) { + const { backendAPI } = config; - const storageDetailData = prepareData(storageDetail); - try { - const response = await Axios.post(`${backendAPI}/cloudstorages`, storageDetailData, { - proxy: config.proxy, - }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } + const storageDetailData = prepareData(storageDetail); + try { + await Axios.patch(`${backendAPI}/cloudstorages/${id}`, storageDetailData, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function updateCloudStorage(id, storageDetail) { - const { backendAPI } = config; + async function getCloudStorages(filter = {}) { + const { backendAPI } = config; - const storageDetailData = prepareData(storageDetail); - try { - await Axios.patch(`${backendAPI}/cloudstorages/${id}`, storageDetailData, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + let response = null; + try { + response = await Axios.get(`${backendAPI}/cloudstorages`, { + proxy: config.proxy, + params: filter, + page_size: 12, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getCloudStorages(filter = {}) { - const { backendAPI } = config; + response.data.results.count = response.data.count; + return response.data.results; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/cloudstorages`, { - proxy: config.proxy, - params: filter, - page_size: 12, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function getCloudStorageContent(id, manifestPath) { + const { backendAPI } = config; - response.data.results.count = response.data.count; - return response.data.results; + let response = null; + try { + const url = `${backendAPI}/cloudstorages/${id}/content${ + manifestPath ? `?manifest_path=${manifestPath}` : '' + }`; + response = await Axios.get(url, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getCloudStorageContent(id, manifestPath) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - const url = `${backendAPI}/cloudstorages/${id}/content${ - manifestPath ? `?manifest_path=${manifestPath}` : '' - }`; - response = await Axios.get(url, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function getCloudStoragePreview(id) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + const url = `${backendAPI}/cloudstorages/${id}/preview`; + response = await workerAxios.get(url, { + params: enableOrganization(), + proxy: config.proxy, + responseType: 'arraybuffer', + }); + } catch (errorData) { + throw generateError({ + message: '', + response: { + ...errorData.response, + data: String.fromCharCode.apply(null, new Uint8Array(errorData.response.data)), + }, + }); } - async function getCloudStoragePreview(id) { - const { backendAPI } = config; + return new Blob([new Uint8Array(response)]); + } - let response = null; - try { - const url = `${backendAPI}/cloudstorages/${id}/preview`; - response = await workerAxios.get(url, { - params: enableOrganization(), - proxy: config.proxy, - responseType: 'arraybuffer', - }); - } catch (errorData) { - throw generateError({ - message: '', - response: { - ...errorData.response, - data: String.fromCharCode.apply(null, new Uint8Array(errorData.response.data)), - }, - }); - } + async function getCloudStorageStatus(id) { + const { backendAPI } = config; - return new Blob([new Uint8Array(response)]); + let response = null; + try { + const url = `${backendAPI}/cloudstorages/${id}/status`; + response = await Axios.get(url, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getCloudStorageStatus(id) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - const url = `${backendAPI}/cloudstorages/${id}/status`; - response = await Axios.get(url, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function deleteCloudStorage(id) { + const { backendAPI } = config; - return response.data; + try { + await Axios.delete(`${backendAPI}/cloudstorages/${id}`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function deleteCloudStorage(id) { - const { backendAPI } = config; + async function getOrganizations() { + const { backendAPI } = config; - try { - await Axios.delete(`${backendAPI}/cloudstorages/${id}`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + let response = null; + try { + response = await Axios.get(`${backendAPI}/organizations`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getOrganizations() { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/organizations`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function createOrganization(data) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.post(`${backendAPI}/organizations`, JSON.stringify(data), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function createOrganization(data) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.post(`${backendAPI}/organizations`, JSON.stringify(data), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function updateOrganization(id, data) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.patch(`${backendAPI}/organizations/${id}`, JSON.stringify(data), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function updateOrganization(id, data) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.patch(`${backendAPI}/organizations/${id}`, JSON.stringify(data), { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function deleteOrganization(id) { + const { backendAPI } = config; - return response.data; + try { + await Axios.delete(`${backendAPI}/organizations/${id}`, { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function deleteOrganization(id) { - const { backendAPI } = config; - - try { - await Axios.delete(`${backendAPI}/organizations/${id}`, { - proxy: config.proxy, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function getOrganizationMembers(orgSlug, page, pageSize, filters = {}) { + const { backendAPI } = config; + + let response = null; + try { + response = await Axios.get(`${backendAPI}/memberships`, { + proxy: config.proxy, + params: { + ...filters, + org: orgSlug, + page, + page_size: pageSize, + }, + }); + } catch (errorData) { + throw generateError(errorData); } - async function getOrganizationMembers(orgSlug, page, pageSize, filters = {}) { - const { backendAPI } = config; + return response.data; + } - let response = null; - try { - response = await Axios.get(`${backendAPI}/memberships`, { + async function inviteOrganizationMembers(orgId, data) { + const { backendAPI } = config; + try { + await Axios.post( + `${backendAPI}/invitations`, + { + ...data, + organization: orgId, + }, + { proxy: config.proxy, - params: { - ...filters, - org: orgSlug, - page, - page_size: pageSize, - }, - }); - } catch (errorData) { - throw generateError(errorData); - } - - return response.data; + }, + ); + } catch (errorData) { + throw generateError(errorData); } + } - async function inviteOrganizationMembers(orgId, data) { - const { backendAPI } = config; - try { - await Axios.post( - `${backendAPI}/invitations`, - { - ...data, - organization: orgId, - }, - { - proxy: config.proxy, - }, - ); - } catch (errorData) { - throw generateError(errorData); - } + async function updateOrganizationMembership(membershipId, data) { + const { backendAPI } = config; + let response = null; + try { + response = await Axios.patch( + `${backendAPI}/memberships/${membershipId}`, + { + ...data, + }, + { + proxy: config.proxy, + }, + ); + } catch (errorData) { + throw generateError(errorData); } - async function updateOrganizationMembership(membershipId, data) { - const { backendAPI } = config; - let response = null; - try { - response = await Axios.patch( - `${backendAPI}/memberships/${membershipId}`, - { - ...data, - }, - { - proxy: config.proxy, - }, - ); - } catch (errorData) { - throw generateError(errorData); - } - - return response.data; - } + return response.data; + } - async function deleteOrganizationMembership(membershipId) { - const { backendAPI } = config; + async function deleteOrganizationMembership(membershipId) { + const { backendAPI } = config; - try { - await Axios.delete(`${backendAPI}/memberships/${membershipId}`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + try { + await Axios.delete(`${backendAPI}/memberships/${membershipId}`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } + } - async function getMembershipInvitation(id) { - const { backendAPI } = config; - - let response = null; - try { - response = await Axios.get(`${backendAPI}/invitations/${id}`, { - proxy: config.proxy, - }); - } catch (errorData) { - throw generateError(errorData); - } + async function getMembershipInvitation(id) { + const { backendAPI } = config; - return response.data; + let response = null; + try { + response = await Axios.get(`${backendAPI}/invitations/${id}`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); } - Object.defineProperties( - this, - Object.freeze({ - server: { - value: Object.freeze({ - about, - share, - formats, - exception, - login, - logout, - changePassword, - requestPasswordReset, - resetPassword, - authorized, - register, - request: serverRequest, - userAgreements, - installedApps, - }), - writable: false, - }, + return response.data; + } - projects: { - value: Object.freeze({ - get: getProjects, - searchNames: searchProjectNames, - save: saveProject, - create: createProject, - delete: deleteProject, - exportDataset: exportDataset('projects'), - backupProject, - restoreProject, - importDataset, - }), - writable: false, - }, + Object.defineProperties( + this, + Object.freeze({ + server: { + value: Object.freeze({ + about, + share, + formats, + exception, + login, + logout, + changePassword, + requestPasswordReset, + resetPassword, + authorized, + register, + request: serverRequest, + userAgreements, + installedApps, + }), + writable: false, + }, - tasks: { - value: Object.freeze({ - get: getTasks, - save: saveTask, - create: createTask, - delete: deleteTask, - exportDataset: exportDataset('tasks'), - export: exportTask, - import: importTask, - }), - writable: false, - }, + projects: { + value: Object.freeze({ + get: getProjects, + searchNames: searchProjectNames, + save: saveProject, + create: createProject, + delete: deleteProject, + exportDataset: exportDataset('projects'), + backup: backupProject, + restore: restoreProject, + importDataset, + }), + writable: false, + }, - jobs: { - value: Object.freeze({ - get: getJobs, - save: saveJob, - }), - writable: false, - }, + tasks: { + value: Object.freeze({ + get: getTasks, + save: saveTask, + create: createTask, + delete: deleteTask, + exportDataset: exportDataset('tasks'), + backup: backupTask, + restore: restoreTask, + }), + writable: false, + }, - users: { - value: Object.freeze({ - get: getUsers, - self: getSelf, - }), - writable: false, - }, + jobs: { + value: Object.freeze({ + get: getJobs, + save: saveJob, + exportDataset: exportDataset('jobs'), + }), + writable: false, + }, - frames: { - value: Object.freeze({ - getData, - getMeta, - saveMeta, - getPreview, - getImageContext, - }), - writable: false, - }, + users: { + value: Object.freeze({ + get: getUsers, + self: getSelf, + }), + writable: false, + }, - annotations: { - value: Object.freeze({ - updateAnnotations, - getAnnotations, - dumpAnnotations, - uploadAnnotations, - }), - writable: false, - }, + frames: { + value: Object.freeze({ + getData, + getMeta, + saveMeta, + getPreview, + getImageContext, + }), + writable: false, + }, - logs: { - value: Object.freeze({ - save: saveLogs, - }), - writable: false, - }, + annotations: { + value: Object.freeze({ + updateAnnotations, + getAnnotations, + dumpAnnotations, + uploadAnnotations, + }), + writable: false, + }, - lambda: { - value: Object.freeze({ - list: getLambdaFunctions, - status: getRequestStatus, - requests: getLambdaRequests, - run: runLambdaRequest, - call: callLambdaFunction, - cancel: cancelLambdaRequest, - }), - writable: false, - }, + logs: { + value: Object.freeze({ + save: saveLogs, + }), + writable: false, + }, - issues: { - value: Object.freeze({ - create: createIssue, - update: updateIssue, - get: getJobIssues, - delete: deleteIssue, - }), - writable: false, - }, + lambda: { + value: Object.freeze({ + list: getLambdaFunctions, + status: getRequestStatus, + requests: getLambdaRequests, + run: runLambdaRequest, + call: callLambdaFunction, + cancel: cancelLambdaRequest, + }), + writable: false, + }, - comments: { - value: Object.freeze({ - create: createComment, - }), - writable: false, - }, + issues: { + value: Object.freeze({ + create: createIssue, + update: updateIssue, + get: getJobIssues, + delete: deleteIssue, + }), + writable: false, + }, - predictor: { - value: Object.freeze({ - status: predictorStatus, - predict: predictAnnotations, - }), - writable: false, - }, + comments: { + value: Object.freeze({ + create: createComment, + }), + writable: false, + }, - cloudStorages: { - value: Object.freeze({ - get: getCloudStorages, - getContent: getCloudStorageContent, - getPreview: getCloudStoragePreview, - getStatus: getCloudStorageStatus, - create: createCloudStorage, - delete: deleteCloudStorage, - update: updateCloudStorage, - }), - writable: false, - }, + predictor: { + value: Object.freeze({ + status: predictorStatus, + predict: predictAnnotations, + }), + writable: false, + }, - organizations: { - value: Object.freeze({ - get: getOrganizations, - create: createOrganization, - update: updateOrganization, - members: getOrganizationMembers, - invitation: getMembershipInvitation, - delete: deleteOrganization, - invite: inviteOrganizationMembers, - updateMembership: updateOrganizationMembership, - deleteMembership: deleteOrganizationMembership, - }), - writable: false, - }, - }), - ); - } + cloudStorages: { + value: Object.freeze({ + get: getCloudStorages, + getContent: getCloudStorageContent, + getPreview: getCloudStoragePreview, + getStatus: getCloudStorageStatus, + create: createCloudStorage, + delete: deleteCloudStorage, + update: updateCloudStorage, + }), + writable: false, + }, + + organizations: { + value: Object.freeze({ + get: getOrganizations, + create: createOrganization, + update: updateOrganization, + members: getOrganizationMembers, + invitation: getMembershipInvitation, + delete: deleteOrganization, + invite: inviteOrganizationMembers, + updateMembership: updateOrganizationMembership, + deleteMembership: deleteOrganizationMembership, + }), + writable: false, + }, + }), + ); } +} - const serverProxy = new ServerProxy(); - module.exports = serverProxy; -})(); +const serverProxy = new ServerProxy(); +export default serverProxy; diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 79a8eac0..601a157f 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -1,2682 +1,2757 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -(() => { - const PluginRegistry = require('./plugins').default; - const loggerStorage = require('./logger-storage'); - const serverProxy = require('./server-proxy'); - const { - getFrame, - deleteFrame, - restoreFrame, - getRanges, - getPreview, - clear: clearFrames, - findNotDeletedFrame, - getContextImage, - patchMeta, - getDeletedFrames, - } = require('./frames'); - const { ArgumentError, DataError } = require('./exceptions'); - const { - JobStage, JobState, HistoryActions, - } = require('./enums'); - const { Label } = require('./labels'); - const User = require('./user'); - const Issue = require('./issue'); - const { FieldUpdateTrigger, checkObjectType } = require('./common'); - - function buildDuplicatedAPI(prototype) { - Object.defineProperties(prototype, { - annotations: Object.freeze({ - value: { - async upload(file, loader) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.upload, - file, - loader, - ); - return result; - }, +import { StorageLocation } from './enums'; +import { Storage } from './storage'; + +const PluginRegistry = require('./plugins').default; +const loggerStorage = require('./logger-storage'); +const serverProxy = require('./server-proxy').default; +const { + getFrame, + deleteFrame, + restoreFrame, + getRanges, + getPreview, + clear: clearFrames, + findNotDeletedFrame, + getContextImage, + patchMeta, + getDeletedFrames, +} = require('./frames'); +const { ArgumentError, DataError } = require('./exceptions'); +const { + JobStage, JobState, HistoryActions, +} = require('./enums'); +const { Label } = require('./labels'); +const User = require('./user'); +const Issue = require('./issue'); +const { FieldUpdateTrigger, checkObjectType } = require('./common'); + +function buildDuplicatedAPI(prototype) { + Object.defineProperties(prototype, { + annotations: Object.freeze({ + value: { + async upload(format: string, useDefaultLocation: boolean, sourceStorage: Storage, file: File | string) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.upload, + format, + useDefaultLocation, + sourceStorage, + file, + ); + return result; + }, - async save(onUpdate) { - const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.save, onUpdate); - return result; - }, + async save(onUpdate) { + const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.save, onUpdate); + return result; + }, - async clear( - reload = false, startframe = undefined, endframe = undefined, delTrackKeyframesOnly = true, - ) { - const result = await PluginRegistry.apiWrapper.call( - this, prototype.annotations.clear, reload, startframe, endframe, delTrackKeyframesOnly, - ); - return result; - }, + async clear( + reload = false, startframe = undefined, endframe = undefined, delTrackKeyframesOnly = true, + ) { + const result = await PluginRegistry.apiWrapper.call( + this, prototype.annotations.clear, reload, startframe, endframe, delTrackKeyframesOnly, + ); + return result; + }, - async statistics() { - const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.statistics); - return result; - }, + async statistics() { + const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.statistics); + return result; + }, - async put(arrayOfObjects = []) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.put, - arrayOfObjects, - ); - return result; - }, + async put(arrayOfObjects = []) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.put, + arrayOfObjects, + ); + return result; + }, - async get(frame, allTracks = false, filters = []) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.get, - frame, - allTracks, - filters, - ); - return result; - }, + async get(frame, allTracks = false, filters = []) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.get, + frame, + allTracks, + filters, + ); + return result; + }, - async search(filters, frameFrom, frameTo) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.search, - filters, - frameFrom, - frameTo, - ); - return result; - }, + async search(filters, frameFrom, frameTo) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.search, + filters, + frameFrom, + frameTo, + ); + return result; + }, - async searchEmpty(frameFrom, frameTo) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.searchEmpty, - frameFrom, - frameTo, - ); - return result; - }, + async searchEmpty(frameFrom, frameTo) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.searchEmpty, + frameFrom, + frameTo, + ); + return result; + }, - async select(objectStates, x, y) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.select, - objectStates, - x, - y, - ); - return result; - }, + async select(objectStates, x, y) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.select, + objectStates, + x, + y, + ); + return result; + }, - async merge(objectStates) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.merge, - objectStates, - ); - return result; - }, + async merge(objectStates) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.merge, + objectStates, + ); + return result; + }, - async split(objectState, frame) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.split, - objectState, - frame, - ); - return result; - }, + async split(objectState, frame) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.split, + objectState, + frame, + ); + return result; + }, - async group(objectStates, reset = false) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.group, - objectStates, - reset, - ); - return result; - }, + async group(objectStates, reset = false) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.group, + objectStates, + reset, + ); + return result; + }, - async import(data) { - const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.import, data); - return result; - }, + async import(data) { + const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.import, data); + return result; + }, - async export() { - const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.export); - return result; - }, + async export() { + const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.export); + return result; + }, - async exportDataset(format, saveImages, customName = '') { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.exportDataset, - format, - saveImages, - customName, - ); - return result; - }, + async exportDataset( + format: string, + saveImages: boolean, + useDefaultSettings: boolean, + targetStorage: Storage, + customName?: string, + ) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.annotations.exportDataset, + format, + saveImages, + useDefaultSettings, + targetStorage, + customName, + ); + return result; + }, - hasUnsavedChanges() { - const result = prototype.annotations.hasUnsavedChanges.implementation.call(this); - return result; - }, + hasUnsavedChanges() { + const result = prototype.annotations.hasUnsavedChanges.implementation.call(this); + return result; }, - writable: true, - }), - frames: Object.freeze({ - value: { - async get(frame, isPlaying = false, step = 1) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.frames.get, - frame, - isPlaying, - step, - ); - return result; - }, - async delete(frame) { - await PluginRegistry.apiWrapper.call( - this, - prototype.frames.delete, - frame, - ); - }, - async restore(frame) { - await PluginRegistry.apiWrapper.call( - this, - prototype.frames.restore, - frame, - ); - }, - async save() { - await PluginRegistry.apiWrapper.call( - this, - prototype.frames.save, - ); - }, - async ranges() { - const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.ranges); - return result; - }, - async preview() { - const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview); - return result; - }, - async search(filters, frameFrom, frameTo) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.frames.search, - filters, - frameFrom, - frameTo, - ); - return result; - }, - async contextImage(frameId) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.frames.contextImage, - frameId, - ); - return result; - }, + }, + writable: true, + }), + frames: Object.freeze({ + value: { + async get(frame, isPlaying = false, step = 1) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.frames.get, + frame, + isPlaying, + step, + ); + return result; }, - writable: true, - }), - logger: Object.freeze({ - value: { - async log(logType, payload = {}, wait = false) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.logger.log, - logType, - payload, - wait, - ); - return result; - }, + async delete(frame) { + await PluginRegistry.apiWrapper.call( + this, + prototype.frames.delete, + frame, + ); }, - writable: true, - }), - actions: Object.freeze({ - value: { - async undo(count = 1) { - const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.undo, count); - return result; - }, - async redo(count = 1) { - const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.redo, count); - return result; - }, - async freeze(frozen) { - const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.freeze, frozen); - return result; - }, - async clear() { - const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.clear); - return result; - }, - async get() { - const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.get); - return result; - }, + async restore(frame) { + await PluginRegistry.apiWrapper.call( + this, + prototype.frames.restore, + frame, + ); }, - writable: true, - }), - events: Object.freeze({ - value: { - async subscribe(evType, callback) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.events.subscribe, - evType, - callback, - ); - return result; - }, - async unsubscribe(evType, callback = null) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.events.unsubscribe, - evType, - callback, - ); - return result; - }, + async save() { + await PluginRegistry.apiWrapper.call( + this, + prototype.frames.save, + ); }, - writable: true, - }), - predictor: Object.freeze({ - value: { - async status() { - const result = await PluginRegistry.apiWrapper.call(this, prototype.predictor.status); - return result; - }, - async predict(frame) { - const result = await PluginRegistry.apiWrapper.call(this, prototype.predictor.predict, frame); - return result; - }, + async ranges() { + const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.ranges); + return result; }, - writable: true, - }), - }); - } - - /** - * Base abstract class for Task and Job. It contains common members. - * @hideconstructor - * @virtual - */ - class Session { - constructor() { - /** - * An interaction with annotations - * @namespace annotations - * @memberof Session - */ - /** - * Upload annotations from a dump file - * You need upload annotations from a server again after successful executing - * @method upload - * @memberof Session.annotations - * @param {File} annotations - a file with annotations - * @param {module:API.cvat.classes.Loader} loader - a loader - * which will be used to upload - * @instance - * @async - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - /** - * Save all changes in annotations on a server - * Objects which hadn't been saved on a server before, - * get a serverID after saving. But received object states aren't updated. - * So, after successful saving it's recommended to update them manually - * (call the annotations.get() again) - * @method save - * @memberof Session.annotations - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - * @instance - * @async - * @param {function} [onUpdate] saving can be long. - * This callback can be used to notify a user about current progress - * Its argument is a text string - */ - /** - * Remove all annotations and optionally reinitialize it - * @method clear - * @memberof Session.annotations - * @param {boolean} [reload = false] reset all changes and - * reinitialize annotations by data from a server - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @throws {module:API.cvat.exceptions.ServerError} - * @instance - * @async - */ - /** - * Collect short statistics about a task or a job. - * @method statistics - * @memberof Session.annotations - * @returns {module:API.cvat.classes.Statistics} statistics object - * @throws {module:API.cvat.exceptions.PluginError} - * @instance - * @async - */ - /** - * Create new objects from one-frame states - * After successful adding you need to update object states on a frame - * @method put - * @memberof Session.annotations - * @param {module:API.cvat.classes.ObjectState[]} data - * @returns {number[]} identificators of added objects - * array of objects on the specific frame - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.DataError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Get annotations for a specific frame - *
    Filter supports following operators: - * ==, !=, >, >=, <, <=, ~= and (), |, & for grouping. - *
    Filter supports properties: - * width, height, label, serverID, clientID, type, shape, occluded - *
    All prop values are case-sensitive. CVAT uses json queries for search. - *
    Examples: - *
      - *
    • label=="car" | label==["road sign"]
    • - *
    • width >= height
    • - *
    • attr["Attribute 1"] == attr["Attribute 2"]
    • - *
    • type=="track" & shape="rectangle"
    • - *
    • clientID == 50
    • - *
    • (label=="car" & attr["parked"]==true) - * | (label=="pedestrian" & width > 150)
    • - *
    • (( label==["car \"mazda\""]) & - * (attr["sunglass ( help ) es"]==true | - * (width > 150 | height > 150 & (clientID == serverID)))))
    • - *
    - * If you have double quotes in your query string, - * please escape them using back slash: \" - * @method get - * @param {number} frame get objects from the frame - * @param {boolean} allTracks show all tracks - * even if they are outside and not keyframe - * @param {any[]} [filters = []] - * get only objects that satisfied to specific filters - * @returns {module:API.cvat.classes.ObjectState[]} - * @memberof Session.annotations - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Find a frame in the range [from, to] - * that contains at least one object satisfied to a filter - * @method search - * @memberof Session.annotations - * @param {ObjectFilter} [filter = []] filter - * @param {number} from lower bound of a search - * @param {number} to upper bound of a search - * @returns {number|null} a frame that contains objects according to the filter - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Find the nearest empty frame without any annotations - * @method searchEmpty - * @memberof Session.annotations - * @param {number} from lower bound of a search - * @param {number} to upper bound of a search - * @returns {number|null} a empty frame according boundaries - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Select shape under a cursor by using minimal distance - * between a cursor and a shape edge or a shape point - * For closed shapes a cursor is placed inside a shape - * @method select - * @memberof Session.annotations - * @param {module:API.cvat.classes.ObjectState[]} objectStates - * objects which can be selected - * @param {float} x horizontal coordinate - * @param {float} y vertical coordinate - * @returns {Object} - * a pair of {state: ObjectState, distance: number} for selected object. - * Pair values can be null if there aren't any sutisfied objects - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Method unites several shapes and tracks into the one - * All shapes must be the same (rectangle, polygon, etc) - * All labels must be the same - * After successful merge you need to update object states on a frame - * @method merge - * @memberof Session.annotations - * @param {module:API.cvat.classes.ObjectState[]} objectStates - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Method splits a track into two parts - * (start frame: previous frame), (frame, last frame) - * After successful split you need to update object states on a frame - * @method split - * @memberof Session.annotations - * @param {module:API.cvat.classes.ObjectState} objectState - * @param {number} frame - * @throws {module:API.cvat.exceptions.ArgumentError} - * @throws {module:API.cvat.exceptions.PluginError} - * @instance - * @async - */ - /** - * Method creates a new group and put all passed objects into it - * After successful split you need to update object states on a frame - * @method group - * @memberof Session.annotations - * @param {module:API.cvat.classes.ObjectState[]} objectStates - * @param {boolean} reset pass "true" to reset group value (set it to 0) - * @returns {number} an ID of created group - * @throws {module:API.cvat.exceptions.ArgumentError} - * @throws {module:API.cvat.exceptions.PluginError} - * @instance - * @async - */ - /** - * Method indicates if there are any changes in - * annotations which haven't been saved on a server - *
    This function cannot be wrapped with a plugin - * @method hasUnsavedChanges - * @memberof Session.annotations - * @returns {boolean} - * @throws {module:API.cvat.exceptions.PluginError} - * @instance - */ - /** - * - * Import raw data in a collection - * @method import - * @memberof Session.annotations - * @param {Object} data - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * - * Export a collection as a row data - * @method export - * @memberof Session.annotations - * @returns {Object} data - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Export as a dataset. - * Method builds a dataset in the specified format. - * @method exportDataset - * @memberof Session.annotations - * @param {module:String} format - a format - * @returns {string} An URL to the dataset file - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Namespace is used for an interaction with frames - * @namespace frames - * @memberof Session - */ - /** - * Get frame by its number - * @method get - * @memberof Session.frames - * @param {number} frame number of frame which you want to get - * @returns {module:API.cvat.classes.FrameData} - * @instance - * @async - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.DataError} - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - /** - * @typedef {Object} FrameSearchFilters - * @property {boolean} notDeleted if true will search for non-deleted frames - * @property {number} offset defines frame step during search - /** - * Find frame that match the condition - * @method search - * @memberof Session.frames - * @param {FrameSearchFilters} filters filters to search frame for - * @param {number} from lower bound of a search - * @param {number} to upper bound of a search - * @returns {number|null} a non-deleted frame according boundaries - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Delete frame from the job - * @method delete - * @memberof Session.frames - * @param {number} frame number of frame which you want to delete - * @throws {module:API.cvat.exceptions.ArgumentError} - * @throws {module:API.cvat.exceptions.PluginError} - * @instance - * @async - */ - /** - * Restore frame from the job - * @method delete - * @memberof Session.frames - * @param {number} frame number of frame which you want to restore - * @throws {module:API.cvat.exceptions.ArgumentError} - * @throws {module:API.cvat.exceptions.PluginError} - * @instance - * @async - */ - /** - * Save any changes in frames if some of them were deleted/restored - * @method save - * @memberof Session.frames - * @throws {module:API.cvat.exceptions.PluginError} - * @instance - * @async - */ - /** - * Get the first frame of a task for preview - * @method preview - * @memberof Session.frames - * @returns {string} - jpeg encoded image - * @instance - * @async - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - /** - * Returns the ranges of cached frames - * @method ranges - * @memberof Session.frames - * @returns {Array.} - * @instance - * @async - */ - /** - * Namespace is used for an interaction with logs - * @namespace logger - * @memberof Session - */ - /** - * Create a log and add it to a log collection
    - * Durable logs will be added after "close" method is called for them
    - * The fields "task_id" and "job_id" automatically added when add logs - * through a task or a job
    - * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute)
    - * Payload of ignored logs are shallowly combined to previous logs of the same type - * @method log - * @memberof Session.logger - * @param {module:API.cvat.enums.LogType | string} type - log type - * @param {Object} [payload = {}] - any other data that will be appended to the log - * @param {boolean} [wait = false] - specifies if log is durable - * @returns {module:API.cvat.classes.Log} - * @instance - * @async - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - /** - * Namespace is used for an interaction with actions - * @namespace actions - * @memberof Session - */ - /** - * @typedef {Object} HistoryActions - * @property {string[]} [undo] - array of possible actions to undo - * @property {string[]} [redo] - array of possible actions to redo - * @global - */ - /** - * Make undo - * @method undo - * @memberof Session.actions - * @param {number} [count=1] number of actions to undo - * @returns {number[]} Array of affected objects - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Make redo - * @method redo - * @memberof Session.actions - * @param {number} [count=1] number of actions to redo - * @returns {number[]} Array of affected objects - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Freeze history (do not save new actions) - * @method freeze - * @memberof Session.actions - * @throws {module:API.cvat.exceptions.PluginError} - * @instance - * @async - */ - /** - * Remove all actions from history - * @method clear - * @memberof Session.actions - * @throws {module:API.cvat.exceptions.PluginError} - * @instance - * @async - */ - /** - * Get actions - * @method get - * @memberof Session.actions - * @returns {HistoryActions} - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @returns {Array.>} - * array of pairs [action name, frame number] - * @instance - * @async - */ - /** - * Namespace is used for an interaction with events - * @namespace events - * @memberof Session - */ - /** - * Subscribe on an event - * @method subscribe - * @memberof Session.events - * @param {module:API.cvat.enums.EventType} type - event type - * @param {functions} callback - function which will be called on event - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * Unsubscribe from an event. If callback is not provided, - * all callbacks will be removed from subscribers for the event - * @method unsubscribe - * @memberof Session.events - * @param {module:API.cvat.enums.EventType} type - event type - * @param {functions} [callback = null] - function which is called on event - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ - /** - * @typedef {Object} PredictorStatus - * @property {string} message - message for a user to be displayed somewhere - * @property {number} projectScore - model accuracy - * @global - */ - /** - * Namespace is used for an interaction with events - * @namespace predictor - * @memberof Session - */ - /** - * Subscribe to updates of a ML model binded to the project - * @method status - * @memberof Session.predictor - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - * @returns {PredictorStatus} - * @instance - * @async - */ - /** - * Get predictions from a ML model binded to the project - * @method predict - * @memberof Session.predictor - * @param {number} frame - number of frame to inference - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.DataError} - * @returns {object[] | null} annotations - * @instance - * @async - */ - } - } - - /** - * Class representing a job. - * @memberof module:API.cvat.classes - * @hideconstructor - * @extends Session - */ - class Job extends Session { - constructor(initialData) { - super(); - const data = { - id: undefined, - assignee: null, - stage: undefined, - state: undefined, - start_frame: undefined, - stop_frame: undefined, - project_id: null, - task_id: undefined, - labels: undefined, - dimension: undefined, - data_compressed_chunk_type: undefined, - data_chunk_size: undefined, - bug_tracker: null, - mode: undefined, - }; - - const updateTrigger = new FieldUpdateTrigger(); - - for (const property in data) { - if (Object.prototype.hasOwnProperty.call(data, property)) { - if (property in initialData) { - data[property] = initialData[property]; - } - - if (data[property] === undefined) { - throw new ArgumentError(`Job field "${property}" was not initialized`); - } - } - } - - if (data.assignee) data.assignee = new User(data.assignee); - if (Array.isArray(initialData.labels)) { - data.labels = initialData.labels.map((labelData) => { - // can be already wrapped to the class - // when create this job from Task constructor - if (labelData instanceof Label) { - return labelData; - } - - return new Label(labelData); - }).filter((label) => !label.hasParent); - } else { - throw new Error('Job labels must be an array'); - } - - Object.defineProperties( - this, - Object.freeze({ - /** - * @name id - * @type {number} - * @memberof module:API.cvat.classes.Job - * @readonly - * @instance - */ - id: { - get: () => data.id, - }, - /** - * Instance of a user who is responsible for the job annotations - * @name assignee - * @type {module:API.cvat.classes.User} - * @memberof module:API.cvat.classes.Job - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - assignee: { - get: () => data.assignee, - set: (assignee) => { - if (assignee !== null && !(assignee instanceof User)) { - throw new ArgumentError('Value must be a user instance'); - } - updateTrigger.update('assignee'); - data.assignee = assignee; - }, - }, - /** - * @name stage - * @type {module:API.cvat.enums.JobStage} - * @memberof module:API.cvat.classes.Job - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - stage: { - get: () => data.stage, - set: (stage) => { - const type = JobStage; - let valueInEnum = false; - for (const value in type) { - if (type[value] === stage) { - valueInEnum = true; - break; - } - } - - if (!valueInEnum) { - throw new ArgumentError( - 'Value must be a value from the enumeration cvat.enums.JobStage', - ); - } - - updateTrigger.update('stage'); - data.stage = stage; - }, - }, - /** - * @name state - * @type {module:API.cvat.enums.JobState} - * @memberof module:API.cvat.classes.Job - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - state: { - get: () => data.state, - set: (state) => { - const type = JobState; - let valueInEnum = false; - for (const value in type) { - if (type[value] === state) { - valueInEnum = true; - break; - } - } - - if (!valueInEnum) { - throw new ArgumentError( - 'Value must be a value from the enumeration cvat.enums.JobState', - ); - } - - updateTrigger.update('state'); - data.state = state; - }, - }, - /** - * @name startFrame - * @type {number} - * @memberof module:API.cvat.classes.Job - * @readonly - * @instance - */ - startFrame: { - get: () => data.start_frame, - }, - /** - * @name stopFrame - * @type {number} - * @memberof module:API.cvat.classes.Job - * @readonly - * @instance - */ - stopFrame: { - get: () => data.stop_frame, - }, - /** - * @name projectId - * @type {number|null} - * @memberof module:API.cvat.classes.Job - * @readonly - * @instance - */ - projectId: { - get: () => data.project_id, - }, - /** - * @name taskId - * @type {number} - * @memberof module:API.cvat.classes.Job - * @readonly - * @instance - */ - taskId: { - get: () => data.task_id, - }, - /** - * @name labels - * @type {module:API.cvat.classes.Label[]} - * @memberof module:API.cvat.classes.Job - * @readonly - * @instance - */ - labels: { - get: () => data.labels.filter((_label) => !_label.deleted), - }, - /** - * @name dimension - * @type {module:API.cvat.enums.DimensionType} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - dimension: { - get: () => data.dimension, - }, - /** - * @name dataChunkSize - * @type {number} - * @memberof module:API.cvat.classes.Job - * @readonly - * @instance - */ - dataChunkSize: { - get: () => data.data_chunk_size, - set: (chunkSize) => { - if (typeof chunkSize !== 'number' || chunkSize < 1) { - throw new ArgumentError( - `Chunk size value must be a positive number. But value ${chunkSize} has been got.`, - ); - } - - data.data_chunk_size = chunkSize; - }, - }, - /** - * @name dataChunkSize - * @type {string} - * @memberof module:API.cvat.classes.Job - * @readonly - * @instance - */ - dataChunkType: { - get: () => data.data_compressed_chunk_type, - }, - /** - * @name mode - * @type {string} - * @memberof module:API.cvat.classes.Job - * @readonly - * @instance - */ - mode: { - get: () => data.mode, - }, - /** - * @name bugTracker - * @type {string|null} - * @memberof module:API.cvat.classes.Job - * @instance - * @readonly - */ - bugTracker: { - get: () => data.bug_tracker, - }, - _updateTrigger: { - get: () => updateTrigger, - }, - }), - ); - - // When we call a function, for example: task.annotations.get() - // In the method get we lose the task context - // So, we need return it - this.annotations = { - get: Object.getPrototypeOf(this).annotations.get.bind(this), - put: Object.getPrototypeOf(this).annotations.put.bind(this), - save: Object.getPrototypeOf(this).annotations.save.bind(this), - merge: Object.getPrototypeOf(this).annotations.merge.bind(this), - split: Object.getPrototypeOf(this).annotations.split.bind(this), - group: Object.getPrototypeOf(this).annotations.group.bind(this), - clear: Object.getPrototypeOf(this).annotations.clear.bind(this), - search: Object.getPrototypeOf(this).annotations.search.bind(this), - searchEmpty: Object.getPrototypeOf(this).annotations.searchEmpty.bind(this), - upload: Object.getPrototypeOf(this).annotations.upload.bind(this), - select: Object.getPrototypeOf(this).annotations.select.bind(this), - import: Object.getPrototypeOf(this).annotations.import.bind(this), - export: Object.getPrototypeOf(this).annotations.export.bind(this), - statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this), - hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this), - exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), - }; - - this.actions = { - undo: Object.getPrototypeOf(this).actions.undo.bind(this), - redo: Object.getPrototypeOf(this).actions.redo.bind(this), - freeze: Object.getPrototypeOf(this).actions.freeze.bind(this), - clear: Object.getPrototypeOf(this).actions.clear.bind(this), - get: Object.getPrototypeOf(this).actions.get.bind(this), - }; - - this.frames = { - get: Object.getPrototypeOf(this).frames.get.bind(this), - delete: Object.getPrototypeOf(this).frames.delete.bind(this), - restore: Object.getPrototypeOf(this).frames.restore.bind(this), - save: Object.getPrototypeOf(this).frames.save.bind(this), - ranges: Object.getPrototypeOf(this).frames.ranges.bind(this), - preview: Object.getPrototypeOf(this).frames.preview.bind(this), - search: Object.getPrototypeOf(this).frames.search.bind(this), - contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this), - }; - - this.logger = { - log: Object.getPrototypeOf(this).logger.log.bind(this), - }; - - this.predictor = { - status: Object.getPrototypeOf(this).predictor.status.bind(this), - predict: Object.getPrototypeOf(this).predictor.predict.bind(this), - }; - } - + async preview() { + const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview); + return result; + }, + async search(filters, frameFrom, frameTo) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.frames.search, + filters, + frameFrom, + frameTo, + ); + return result; + }, + async contextImage(frameId) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.frames.contextImage, + frameId, + ); + return result; + }, + }, + writable: true, + }), + logger: Object.freeze({ + value: { + async log(logType, payload = {}, wait = false) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.logger.log, + logType, + payload, + wait, + ); + return result; + }, + }, + writable: true, + }), + actions: Object.freeze({ + value: { + async undo(count = 1) { + const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.undo, count); + return result; + }, + async redo(count = 1) { + const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.redo, count); + return result; + }, + async freeze(frozen) { + const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.freeze, frozen); + return result; + }, + async clear() { + const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.clear); + return result; + }, + async get() { + const result = await PluginRegistry.apiWrapper.call(this, prototype.actions.get); + return result; + }, + }, + writable: true, + }), + events: Object.freeze({ + value: { + async subscribe(evType, callback) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.events.subscribe, + evType, + callback, + ); + return result; + }, + async unsubscribe(evType, callback = null) { + const result = await PluginRegistry.apiWrapper.call( + this, + prototype.events.unsubscribe, + evType, + callback, + ); + return result; + }, + }, + writable: true, + }), + predictor: Object.freeze({ + value: { + async status() { + const result = await PluginRegistry.apiWrapper.call(this, prototype.predictor.status); + return result; + }, + async predict(frame) { + const result = await PluginRegistry.apiWrapper.call(this, prototype.predictor.predict, frame); + return result; + }, + }, + writable: true, + }), + }); +} + +/** + * Base abstract class for Task and Job. It contains common members. + * @hideconstructor + * @virtual + */ +export class Session { + constructor() { + /** + * An interaction with annotations + * @namespace annotations + * @memberof Session + */ + /** + * Upload annotations from a dump file + * You need upload annotations from a server again after successful executing + * @method upload + * @memberof Session.annotations + * @param {File} annotations - a file with annotations + * @param {module:API.cvat.classes.Loader} loader - a loader + * which will be used to upload + * @instance + * @async + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.ArgumentError} + */ /** - * Method updates job data like state, stage or assignee + * Save all changes in annotations on a server + * Objects which hadn't been saved on a server before, + * get a serverID after saving. But received object states aren't updated. + * So, after successful saving it's recommended to update them manually + * (call the annotations.get() again) * @method save - * @memberof module:API.cvat.classes.Job - * @readonly + * @memberof Session.annotations + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} * @instance * @async + * @param {function} [onUpdate] saving can be long. + * This callback can be used to notify a user about current progress + * Its argument is a text string + */ + /** + * Remove all annotations and optionally reinitialize it + * @method clear + * @memberof Session.annotations + * @param {boolean} [reload = false] reset all changes and + * reinitialize annotations by data from a server + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ServerError} + * @instance + * @async + */ + /** + * Collect short statistics about a task or a job. + * @method statistics + * @memberof Session.annotations + * @returns {module:API.cvat.classes.Statistics} statistics object * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async */ - async save() { - const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.save); - return result; - } - /** - * Method returns a list of issues for a job - * @method issues - * @memberof module:API.cvat.classes.Job - * @returns {module:API.cvat.classes.Issue[]} - * @readonly + * Create new objects from one-frame states + * After successful adding you need to update object states on a frame + * @method put + * @memberof Session.annotations + * @param {module:API.cvat.classes.ObjectState[]} data + * @returns {number[]} identificators of added objects + * array of objects on the specific frame + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.DataError} + * @throws {module:API.cvat.exceptions.ArgumentError} * @instance * @async - * @throws {module:API.cvat.exceptions.ServerError} + */ + /** + * Get annotations for a specific frame + *
    Filter supports following operators: + * ==, !=, >, >=, <, <=, ~= and (), |, & for grouping. + *
    Filter supports properties: + * width, height, label, serverID, clientID, type, shape, occluded + *
    All prop values are case-sensitive. CVAT uses json queries for search. + *
    Examples: + *
      + *
    • label=="car" | label==["road sign"]
    • + *
    • width >= height
    • + *
    • attr["Attribute 1"] == attr["Attribute 2"]
    • + *
    • type=="track" & shape="rectangle"
    • + *
    • clientID == 50
    • + *
    • (label=="car" & attr["parked"]==true) + * | (label=="pedestrian" & width > 150)
    • + *
    • (( label==["car \"mazda\""]) & + * (attr["sunglass ( help ) es"]==true | + * (width > 150 | height > 150 & (clientID == serverID)))))
    • + *
    + * If you have double quotes in your query string, + * please escape them using back slash: \" + * @method get + * @param {number} frame get objects from the frame + * @param {boolean} allTracks show all tracks + * even if they are outside and not keyframe + * @param {any[]} [filters = []] + * get only objects that satisfied to specific filters + * @returns {module:API.cvat.classes.ObjectState[]} + * @memberof Session.annotations * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance + * @async */ - async issues() { - const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.issues); - return result; - } - /** - * Method adds a new issue to a job - * @method openIssue - * @memberof module:API.cvat.classes.Job - * @returns {module:API.cvat.classes.Issue} - * @param {module:API.cvat.classes.Issue} issue - * @param {string} message - * @readonly + * Find a frame in the range [from, to] + * that contains at least one object satisfied to a filter + * @method search + * @memberof Session.annotations + * @param {ObjectFilter} [filter = []] filter + * @param {number} from lower bound of a search + * @param {number} to upper bound of a search + * @returns {number|null} a frame that contains objects according to the filter + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} * @instance * @async + */ + /** + * Find the nearest empty frame without any annotations + * @method searchEmpty + * @memberof Session.annotations + * @param {number} from lower bound of a search + * @param {number} to upper bound of a search + * @returns {number|null} a empty frame according boundaries + * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ArgumentError} - * @throws {module:API.cvat.exceptions.ServerError} + * @instance + * @async + */ + /** + * Select shape under a cursor by using minimal distance + * between a cursor and a shape edge or a shape point + * For closed shapes a cursor is placed inside a shape + * @method select + * @memberof Session.annotations + * @param {module:API.cvat.classes.ObjectState[]} objectStates + * objects which can be selected + * @param {float} x horizontal coordinate + * @param {float} y vertical coordinate + * @returns {Object} + * a pair of {state: ObjectState, distance: number} for selected object. + * Pair values can be null if there aren't any sutisfied objects * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance + * @async */ - async openIssue(issue, message) { - const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.openIssue, issue, message); - return result; - } - /** - * Method removes all job related data from the client (annotations, history, etc.) - * @method close - * @returns {module:API.cvat.classes.Job} - * @memberof module:API.cvat.classes.Job - * @readonly + * Method unites several shapes and tracks into the one + * All shapes must be the same (rectangle, polygon, etc) + * All labels must be the same + * After successful merge you need to update object states on a frame + * @method merge + * @memberof Session.annotations + * @param {module:API.cvat.classes.ObjectState[]} objectStates + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance * @async + */ + /** + * Method splits a track into two parts + * (start frame: previous frame), (frame, last frame) + * After successful split you need to update object states on a frame + * @method split + * @memberof Session.annotations + * @param {module:API.cvat.classes.ObjectState} objectState + * @param {number} frame + * @throws {module:API.cvat.exceptions.ArgumentError} + * @throws {module:API.cvat.exceptions.PluginError} * @instance + * @async + */ + /** + * Method creates a new group and put all passed objects into it + * After successful split you need to update object states on a frame + * @method group + * @memberof Session.annotations + * @param {module:API.cvat.classes.ObjectState[]} objectStates + * @param {boolean} reset pass "true" to reset group value (set it to 0) + * @returns {number} an ID of created group + * @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async */ - async close() { - const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.close); - return result; - } - } - - /** - * Class representing a task - * @memberof module:API.cvat.classes - * @extends Session - */ - class Task extends Session { /** - * In a fact you need use the constructor only if you want to create a task - * @param {object} initialData - Object which is used for initialization - *
    It can contain keys: - *
  • name - *
  • assignee - *
  • bug_tracker - *
  • labels - *
  • segment_size - *
  • overlap + * Method indicates if there are any changes in + * annotations which haven't been saved on a server + *
    This function cannot be wrapped with a plugin + * @method hasUnsavedChanges + * @memberof Session.annotations + * @returns {boolean} + * @throws {module:API.cvat.exceptions.PluginError} + * @instance + */ + /** + * + * Import raw data in a collection + * @method import + * @memberof Session.annotations + * @param {Object} data + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance + * @async + */ + /** + * + * Export a collection as a row data + * @method export + * @memberof Session.annotations + * @returns {Object} data + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance + * @async + */ + /** + * Export as a dataset. + * Method builds a dataset in the specified format. + * @method exportDataset + * @memberof Session.annotations + * @param {module:String} format - a format + * @returns {string} An URL to the dataset file + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance + * @async + */ + /** + * Namespace is used for an interaction with frames + * @namespace frames + * @memberof Session + */ + /** + * Get frame by its number + * @method get + * @memberof Session.frames + * @param {number} frame number of frame which you want to get + * @returns {module:API.cvat.classes.FrameData} + * @instance + * @async + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.DataError} + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + /** + * @typedef {Object} FrameSearchFilters + * @property {boolean} notDeleted if true will search for non-deleted frames + * @property {number} offset defines frame step during search + /** + * Find frame that match the condition + * @method search + * @memberof Session.frames + * @param {FrameSearchFilters} filters filters to search frame for + * @param {number} from lower bound of a search + * @param {number} to upper bound of a search + * @returns {number|null} a non-deleted frame according boundaries + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance + * @async + */ + /** + * Delete frame from the job + * @method delete + * @memberof Session.frames + * @param {number} frame number of frame which you want to delete + * @throws {module:API.cvat.exceptions.ArgumentError} + * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async + */ + /** + * Restore frame from the job + * @method delete + * @memberof Session.frames + * @param {number} frame number of frame which you want to restore + * @throws {module:API.cvat.exceptions.ArgumentError} + * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async + */ + /** + * Save any changes in frames if some of them were deleted/restored + * @method save + * @memberof Session.frames + * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async */ - constructor(initialData) { - super(); - const data = { - id: undefined, - name: undefined, - project_id: null, - status: undefined, - size: undefined, - mode: undefined, - owner: null, - assignee: null, - created_date: undefined, - updated_date: undefined, - bug_tracker: undefined, - subset: undefined, - overlap: undefined, - segment_size: undefined, - image_quality: undefined, - start_frame: undefined, - stop_frame: undefined, - frame_filter: undefined, - data_chunk_size: undefined, - data_compressed_chunk_type: undefined, - data_original_chunk_type: undefined, - deleted_frames: undefined, - use_zip_chunks: undefined, - use_cache: undefined, - copy_data: undefined, - dimension: undefined, - cloud_storage_id: undefined, - sorting_method: undefined, - }; - - const updateTrigger = new FieldUpdateTrigger(); - - for (const property in data) { - if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { - data[property] = initialData[property]; - } - } - - if (data.assignee) data.assignee = new User(data.assignee); - if (data.owner) data.owner = new User(data.owner); - - data.labels = []; - data.jobs = []; - data.files = Object.freeze({ - server_files: [], - client_files: [], - remote_files: [], - }); - - if (Array.isArray(initialData.labels)) { - data.labels = initialData.labels - .map((labelData) => new Label(labelData)).filter((label) => !label.hasParent); - } - - if (Array.isArray(initialData.segments)) { - for (const segment of initialData.segments) { - if (Array.isArray(segment.jobs)) { - for (const job of segment.jobs) { - const jobInstance = new Job({ - url: job.url, - id: job.id, - assignee: job.assignee, - state: job.state, - stage: job.stage, - start_frame: segment.start_frame, - stop_frame: segment.stop_frame, - // following fields also returned when doing API request /jobs/ - // here we know them from task and append to constructor - task_id: data.id, - project_id: data.project_id, - labels: data.labels, - bug_tracker: data.bug_tracker, - mode: data.mode, - dimension: data.dimension, - data_compressed_chunk_type: data.data_compressed_chunk_type, - data_chunk_size: data.data_chunk_size, - }); - - data.jobs.push(jobInstance); - } - } - } - } - - Object.defineProperties( - this, - Object.freeze({ - /** - * @name id - * @type {number} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - id: { - get: () => data.id, - }, - /** - * @name name - * @type {string} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - name: { - get: () => data.name, - set: (value) => { - if (!value.trim().length) { - throw new ArgumentError('Value must not be empty'); - } - updateTrigger.update('name'); - data.name = value; - }, - }, - /** - * @name projectId - * @type {number|null} - * @memberof module:API.cvat.classes.Task - * @instance - */ - projectId: { - get: () => data.project_id, - set: (projectId) => { - if (!Number.isInteger(projectId) || projectId <= 0) { - throw new ArgumentError('Value must be a positive integer'); - } - - updateTrigger.update('projectId'); - data.project_id = projectId; - }, - }, - /** - * @name status - * @type {module:API.cvat.enums.TaskStatus} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - status: { - get: () => data.status, - }, - /** - * @name size - * @type {number} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - size: { - get: () => data.size, - }, - /** - * @name mode - * @type {TaskMode} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - mode: { - get: () => data.mode, - }, - /** - * Instance of a user who has created the task - * @name owner - * @type {module:API.cvat.classes.User} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - owner: { - get: () => data.owner, - }, - /** - * Instance of a user who is responsible for the task - * @name assignee - * @type {module:API.cvat.classes.User} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - assignee: { - get: () => data.assignee, - set: (assignee) => { - if (assignee !== null && !(assignee instanceof User)) { - throw new ArgumentError('Value must be a user instance'); - } - updateTrigger.update('assignee'); - data.assignee = assignee; - }, - }, - /** - * @name createdDate - * @type {string} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - createdDate: { - get: () => data.created_date, - }, - /** - * @name updatedDate - * @type {string} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - updatedDate: { - get: () => data.updated_date, - }, - /** - * @name bugTracker - * @type {string} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - bugTracker: { - get: () => data.bug_tracker, - set: (tracker) => { - if (typeof tracker !== 'string') { - throw new ArgumentError( - `Subset value must be a string. But ${typeof tracker} has been got.`, - ); - } - - updateTrigger.update('bugTracker'); - data.bug_tracker = tracker; - }, - }, - /** - * @name subset - * @type {string} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exception.ArgumentError} - */ - subset: { - get: () => data.subset, - set: (subset) => { - if (typeof subset !== 'string') { - throw new ArgumentError( - `Subset value must be a string. But ${typeof subset} has been got.`, - ); - } - - updateTrigger.update('subset'); - data.subset = subset; - }, - }, - /** - * @name overlap - * @type {number} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - overlap: { - get: () => data.overlap, - set: (overlap) => { - if (!Number.isInteger(overlap) || overlap < 0) { - throw new ArgumentError('Value must be a non negative integer'); - } - data.overlap = overlap; - }, - }, - /** - * @name segmentSize - * @type {number} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - segmentSize: { - get: () => data.segment_size, - set: (segment) => { - if (!Number.isInteger(segment) || segment < 0) { - throw new ArgumentError('Value must be a positive integer'); - } - data.segment_size = segment; - }, - }, - /** - * @name imageQuality - * @type {number} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - imageQuality: { - get: () => data.image_quality, - set: (quality) => { - if (!Number.isInteger(quality) || quality < 0) { - throw new ArgumentError('Value must be a positive integer'); - } - data.image_quality = quality; - }, - }, - /** - * @name useZipChunks - * @type {boolean} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - useZipChunks: { - get: () => data.use_zip_chunks, - set: (useZipChunks) => { - if (typeof useZipChunks !== 'boolean') { - throw new ArgumentError('Value must be a boolean'); - } - data.use_zip_chunks = useZipChunks; - }, - }, - /** - * @name useCache - * @type {boolean} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - useCache: { - get: () => data.use_cache, - set: (useCache) => { - if (typeof useCache !== 'boolean') { - throw new ArgumentError('Value must be a boolean'); - } - data.use_cache = useCache; - }, - }, - /** - * @name copyData - * @type {boolean} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - copyData: { - get: () => data.copy_data, - set: (copyData) => { - if (typeof copyData !== 'boolean') { - throw new ArgumentError('Value must be a boolean'); - } - data.copy_data = copyData; - }, - }, - /** - * @name labels - * @type {module:API.cvat.classes.Label[]} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - labels: { - get: () => data.labels.filter((_label) => !_label.deleted), - set: (labels) => { - if (!Array.isArray(labels)) { - throw new ArgumentError('Value must be an array of Labels'); - } - - for (const label of labels) { - if (!(label instanceof Label)) { - throw new ArgumentError( - `Each array value must be an instance of Label. ${typeof label} was found`, - ); - } - } - - const IDs = labels.map((_label) => _label.id); - const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id)); - deletedLabels.forEach((_label) => { - _label.deleted = true; - }); - - updateTrigger.update('labels'); - data.labels = [...deletedLabels, ...labels]; - }, - }, - /** - * @name jobs - * @type {module:API.cvat.classes.Job[]} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - jobs: { - get: () => [...data.jobs], - }, - /** - * List of files from shared resource or list of cloud storage files - * @name serverFiles - * @type {string[]} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - serverFiles: { - get: () => Array.from(data.files.server_files), - set: (serverFiles: string[]) => { - if (!Array.isArray(serverFiles)) { - throw new ArgumentError( - `Value must be an array. But ${typeof serverFiles} has been got.`, - ); - } - - for (const value of serverFiles) { - if (typeof value !== 'string') { - throw new ArgumentError( - `Array values must be a string. But ${typeof value} has been got.`, - ); - } - } - - for (const file of serverFiles) { - data.files.server_files.push(file); - } - }, - }, - /** - * List of files from client host - * @name clientFiles - * @type {File[]} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - clientFiles: { - get: () => Array.from(data.files.client_files), - set: (clientFiles: File[]) => { - if (!Array.isArray(clientFiles)) { - throw new ArgumentError( - `Value must be an array. But ${typeof clientFiles} has been got.`, - ); - } - - for (const value of clientFiles) { - if (!(value instanceof File)) { - throw new ArgumentError( - `Array values must be a File. But ${value.constructor.name} has been got.`, - ); - } - } - - for (const file of clientFiles) { - data.files.client_files.push(file); - } - }, - }, - /** - * List of files from remote host - * @name remoteFiles - * @type {string[]} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - remoteFiles: { - get: () => Array.from(data.files.remote_files), - set: (remoteFiles) => { - if (!Array.isArray(remoteFiles)) { - throw new ArgumentError( - `Value must be an array. But ${typeof remoteFiles} has been got.`, - ); - } - - for (const value of remoteFiles) { - if (typeof value !== 'string') { - throw new ArgumentError( - `Array values must be a string. But ${typeof value} has been got.`, - ); - } - } - - for (const file of remoteFiles) { - data.files.remote_files.push(file); - } - }, - }, - /** - * The first frame of a video to annotation - * @name startFrame - * @type {number} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - startFrame: { - get: () => data.start_frame, - set: (frame) => { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError('Value must be a not negative integer'); - } - data.start_frame = frame; - }, - }, - /** - * The last frame of a video to annotation - * @name stopFrame - * @type {number} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - stopFrame: { - get: () => data.stop_frame, - set: (frame) => { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError('Value must be a not negative integer'); - } - data.stop_frame = frame; - }, - }, - /** - * Filter to ignore some frames during task creation - * @name frameFilter - * @type {string} - * @memberof module:API.cvat.classes.Task - * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} - */ - frameFilter: { - get: () => data.frame_filter, - set: (filter) => { - if (typeof filter !== 'string') { - throw new ArgumentError( - `Filter value must be a string. But ${typeof filter} has been got.`, - ); - } - - data.frame_filter = filter; - }, - }, - dataChunkSize: { - get: () => data.data_chunk_size, - set: (chunkSize) => { - if (typeof chunkSize !== 'number' || chunkSize < 1) { - throw new ArgumentError( - `Chunk size value must be a positive number. But value ${chunkSize} has been got.`, - ); - } - - data.data_chunk_size = chunkSize; - }, - }, - dataChunkType: { - get: () => data.data_compressed_chunk_type, - }, - /** - * @name dimension - * @type {module:API.cvat.enums.DimensionType} - * @memberof module:API.cvat.classes.Task - * @readonly - * @instance - */ - dimension: { - get: () => data.dimension, - }, - /** - * @name cloudStorageId - * @type {integer|null} - * @memberof module:API.cvat.classes.Task - * @instance - */ - cloudStorageId: { - get: () => data.cloud_storage_id, - }, - sortingMethod: { - /** - * @name sortingMethod - * @type {module:API.cvat.enums.SortingMethod} - * @memberof module:API.cvat.classes.Task - * @instance - * @readonly - */ - get: () => data.sorting_method, - }, - _internalData: { - get: () => data, - }, - _updateTrigger: { - get: () => updateTrigger, - }, - }), - ); - - // When we call a function, for example: task.annotations.get() - // In the method get we lose the task context - // So, we need return it - this.annotations = { - get: Object.getPrototypeOf(this).annotations.get.bind(this), - put: Object.getPrototypeOf(this).annotations.put.bind(this), - save: Object.getPrototypeOf(this).annotations.save.bind(this), - merge: Object.getPrototypeOf(this).annotations.merge.bind(this), - split: Object.getPrototypeOf(this).annotations.split.bind(this), - group: Object.getPrototypeOf(this).annotations.group.bind(this), - clear: Object.getPrototypeOf(this).annotations.clear.bind(this), - search: Object.getPrototypeOf(this).annotations.search.bind(this), - searchEmpty: Object.getPrototypeOf(this).annotations.searchEmpty.bind(this), - upload: Object.getPrototypeOf(this).annotations.upload.bind(this), - select: Object.getPrototypeOf(this).annotations.select.bind(this), - import: Object.getPrototypeOf(this).annotations.import.bind(this), - export: Object.getPrototypeOf(this).annotations.export.bind(this), - statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this), - hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this), - exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), - }; - - this.actions = { - undo: Object.getPrototypeOf(this).actions.undo.bind(this), - redo: Object.getPrototypeOf(this).actions.redo.bind(this), - freeze: Object.getPrototypeOf(this).actions.freeze.bind(this), - clear: Object.getPrototypeOf(this).actions.clear.bind(this), - get: Object.getPrototypeOf(this).actions.get.bind(this), - }; - - this.frames = { - get: Object.getPrototypeOf(this).frames.get.bind(this), - delete: Object.getPrototypeOf(this).frames.delete.bind(this), - restore: Object.getPrototypeOf(this).frames.restore.bind(this), - save: Object.getPrototypeOf(this).frames.save.bind(this), - ranges: Object.getPrototypeOf(this).frames.ranges.bind(this), - preview: Object.getPrototypeOf(this).frames.preview.bind(this), - contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this), - search: Object.getPrototypeOf(this).frames.search.bind(this), - }; - - this.logger = { - log: Object.getPrototypeOf(this).logger.log.bind(this), - }; - - this.predictor = { - status: Object.getPrototypeOf(this).predictor.status.bind(this), - predict: Object.getPrototypeOf(this).predictor.predict.bind(this), - }; - } - /** - * Method removes all task related data from the client (annotations, history, etc.) - * @method close - * @returns {module:API.cvat.classes.Task} - * @memberof module:API.cvat.classes.Task - * @readonly + * Get the first frame of a task for preview + * @method preview + * @memberof Session.frames + * @returns {string} - jpeg encoded image + * @instance * @async + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + /** + * Returns the ranges of cached frames + * @method ranges + * @memberof Session.frames + * @returns {Array.} + * @instance + * @async + */ + /** + * Namespace is used for an interaction with logs + * @namespace logger + * @memberof Session + */ + /** + * Create a log and add it to a log collection
    + * Durable logs will be added after "close" method is called for them
    + * The fields "task_id" and "job_id" automatically added when add logs + * through a task or a job
    + * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute)
    + * Payload of ignored logs are shallowly combined to previous logs of the same type + * @method log + * @memberof Session.logger + * @param {module:API.cvat.enums.LogType | string} type - log type + * @param {Object} [payload = {}] - any other data that will be appended to the log + * @param {boolean} [wait = false] - specifies if log is durable + * @returns {module:API.cvat.classes.Log} * @instance + * @async * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + /** + * Namespace is used for an interaction with actions + * @namespace actions + * @memberof Session + */ + /** + * @typedef {Object} HistoryActions + * @property {string[]} [undo] - array of possible actions to undo + * @property {string[]} [redo] - array of possible actions to redo + * @global */ - async close() { - const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.close); - return result; - } - /** - * Method updates data of a created task or creates new task from scratch - * @method save - * @returns {module:API.cvat.classes.Task} - * @memberof module:API.cvat.classes.Task - * @param {function} [onUpdate] - the function which is used only if task hasn't - * been created yet. It called in order to notify about creation status. - * It receives the string parameter which is a status message - * @readonly + * Make undo + * @method undo + * @memberof Session.actions + * @param {number} [count=1] number of actions to undo + * @returns {number[]} Array of affected objects + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} * @instance * @async - * @throws {module:API.cvat.exceptions.ServerError} + */ + /** + * Make redo + * @method redo + * @memberof Session.actions + * @param {number} [count=1] number of actions to redo + * @returns {number[]} Array of affected objects * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance + * @async */ - async save(onUpdate = () => {}) { - const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.save, onUpdate); - return result; - } - /** - * Method deletes a task from a server - * @method delete - * @memberof module:API.cvat.classes.Task - * @readonly + * Freeze history (do not save new actions) + * @method freeze + * @memberof Session.actions + * @throws {module:API.cvat.exceptions.PluginError} * @instance * @async - * @throws {module:API.cvat.exceptions.ServerError} + */ + /** + * Remove all actions from history + * @method clear + * @memberof Session.actions * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async */ - async delete() { - const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.delete); - return result; - } - /** - * Method makes a backup of a task - * @method export - * @memberof module:API.cvat.classes.Task - * @readonly + * Get actions + * @method get + * @memberof Session.actions + * @returns {HistoryActions} + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @returns {Array.>} + * array of pairs [action name, frame number] * @instance * @async - * @throws {module:API.cvat.exceptions.ServerError} + */ + /** + * Namespace is used for an interaction with events + * @namespace events + * @memberof Session + */ + /** + * Subscribe on an event + * @method subscribe + * @memberof Session.events + * @param {module:API.cvat.enums.EventType} type - event type + * @param {functions} callback - function which will be called on event * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance + * @async */ - async export() { - const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.export); - return result; - } - /** - * Method imports a task from a backup - * @method import - * @memberof module:API.cvat.classes.Task - * @readonly + * Unsubscribe from an event. If callback is not provided, + * all callbacks will be removed from subscribers for the event + * @method unsubscribe + * @memberof Session.events + * @param {module:API.cvat.enums.EventType} type - event type + * @param {functions} [callback = null] - function which is called on event + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} * @instance * @async + */ + /** + * @typedef {Object} PredictorStatus + * @property {string} message - message for a user to be displayed somewhere + * @property {number} projectScore - model accuracy + * @global + */ + /** + * Namespace is used for an interaction with events + * @namespace predictor + * @memberof Session + */ + /** + * Subscribe to updates of a ML model binded to the project + * @method status + * @memberof Session.predictor + * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ServerError} + * @returns {PredictorStatus} + * @instance + * @async + */ + /** + * Get predictions from a ML model binded to the project + * @method predict + * @memberof Session.predictor + * @param {number} frame - number of frame to inference * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.DataError} + * @returns {object[] | null} annotations + * @instance + * @async */ - static async import(file) { - const result = await PluginRegistry.apiWrapper.call(this, Task.import, file); - return result; - } } +} + +/** + * Class representing a job. + * @memberof module:API.cvat.classes + * @hideconstructor + * @extends Session + */ +export class Job extends Session { + constructor(initialData) { + super(); + const data = { + id: undefined, + assignee: null, + stage: undefined, + state: undefined, + start_frame: undefined, + stop_frame: undefined, + project_id: null, + task_id: undefined, + labels: undefined, + dimension: undefined, + data_compressed_chunk_type: undefined, + data_chunk_size: undefined, + bug_tracker: null, + mode: undefined, + }; - module.exports = { - Job, - Task, - }; + const updateTrigger = new FieldUpdateTrigger(); - const { - getAnnotations, - putAnnotations, - saveAnnotations, - hasUnsavedChanges, - searchAnnotations, - searchEmptyFrame, - mergeAnnotations, - splitAnnotations, - groupAnnotations, - clearAnnotations, - selectObject, - annotationsStatistics, - uploadAnnotations, - importAnnotations, - exportAnnotations, - exportDataset, - undoActions, - redoActions, - freezeHistory, - clearActions, - getActions, - closeSession, - getHistory, - } = require('./annotations'); - - buildDuplicatedAPI(Job.prototype); - buildDuplicatedAPI(Task.prototype); - - Job.prototype.save.implementation = async function () { - if (this.id) { - const jobData = this._updateTrigger.getUpdated(this); - if (jobData.assignee) { - jobData.assignee = jobData.assignee.id; - } + for (const property in data) { + if (Object.prototype.hasOwnProperty.call(data, property)) { + if (property in initialData) { + data[property] = initialData[property]; + } - const data = await serverProxy.jobs.save(this.id, jobData); - this._updateTrigger.reset(); - return new Job(data); + if (data[property] === undefined) { + throw new ArgumentError(`Job field "${property}" was not initialized`); + } + } } - throw new ArgumentError('Could not save job without id'); - }; + if (data.assignee) data.assignee = new User(data.assignee); + if (Array.isArray(initialData.labels)) { + data.labels = initialData.labels.map((labelData) => { + // can be already wrapped to the class + // when create this job from Task constructor + if (labelData instanceof Label) { + return labelData; + } - Job.prototype.issues.implementation = async function () { - const result = await serverProxy.issues.get(this.id); - return result.map((issue) => new Issue(issue)); - }; + return new Label(labelData); + }).filter((label) => !label.hasParent); + } else { + throw new Error('Job labels must be an array'); + } + + Object.defineProperties( + this, + Object.freeze({ + /** + * @name id + * @type {number} + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + */ + id: { + get: () => data.id, + }, + /** + * Instance of a user who is responsible for the job annotations + * @name assignee + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.Job + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + assignee: { + get: () => data.assignee, + set: (assignee) => { + if (assignee !== null && !(assignee instanceof User)) { + throw new ArgumentError('Value must be a user instance'); + } + updateTrigger.update('assignee'); + data.assignee = assignee; + }, + }, + /** + * @name stage + * @type {module:API.cvat.enums.JobStage} + * @memberof module:API.cvat.classes.Job + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + stage: { + get: () => data.stage, + set: (stage) => { + const type = JobStage; + let valueInEnum = false; + for (const value in type) { + if (type[value] === stage) { + valueInEnum = true; + break; + } + } - 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); - }; + if (!valueInEnum) { + throw new ArgumentError( + 'Value must be a value from the enumeration cvat.enums.JobStage', + ); + } - 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}"`); - } + updateTrigger.update('stage'); + data.stage = stage; + }, + }, + /** + * @name state + * @type {module:API.cvat.enums.JobState} + * @memberof module:API.cvat.classes.Job + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + state: { + get: () => data.state, + set: (state) => { + const type = JobState; + let valueInEnum = false; + for (const value in type) { + if (type[value] === state) { + valueInEnum = true; + break; + } + } - if (frame < this.startFrame || frame > this.stopFrame) { - throw new ArgumentError(`The frame with number ${frame} is out of the job`); - } + if (!valueInEnum) { + throw new ArgumentError( + 'Value must be a value from the enumeration cvat.enums.JobState', + ); + } - const frameData = await getFrame( - this.id, - this.dataChunkSize, - this.dataChunkType, - this.mode, - frame, - this.startFrame, - this.stopFrame, - isPlaying, - step, - this.dimension, + updateTrigger.update('state'); + data.state = state; + }, + }, + /** + * @name startFrame + * @type {number} + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + */ + startFrame: { + get: () => data.start_frame, + }, + /** + * @name stopFrame + * @type {number} + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + */ + stopFrame: { + get: () => data.stop_frame, + }, + /** + * @name projectId + * @type {number|null} + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + */ + projectId: { + get: () => data.project_id, + }, + /** + * @name taskId + * @type {number} + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + */ + taskId: { + get: () => data.task_id, + }, + /** + * @name labels + * @type {module:API.cvat.classes.Label[]} + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + */ + labels: { + get: () => data.labels.filter((_label) => !_label.deleted), + }, + /** + * @name dimension + * @type {module:API.cvat.enums.DimensionType} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + dimension: { + get: () => data.dimension, + }, + /** + * @name dataChunkSize + * @type {number} + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + */ + dataChunkSize: { + get: () => data.data_chunk_size, + set: (chunkSize) => { + if (typeof chunkSize !== 'number' || chunkSize < 1) { + throw new ArgumentError( + `Chunk size value must be a positive number. But value ${chunkSize} has been got.`, + ); + } + + data.data_chunk_size = chunkSize; + }, + }, + /** + * @name dataChunkSize + * @type {string} + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + */ + dataChunkType: { + get: () => data.data_compressed_chunk_type, + }, + /** + * @name mode + * @type {string} + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + */ + mode: { + get: () => data.mode, + }, + /** + * @name bugTracker + * @type {string|null} + * @memberof module:API.cvat.classes.Job + * @instance + * @readonly + */ + bugTracker: { + get: () => data.bug_tracker, + }, + _updateTrigger: { + get: () => updateTrigger, + }, + }), ); - return frameData; - }; - // must be called with task/job context - async function deleteFrameWrapper(jobID, frame) { - const history = getHistory(this); - const redo = async () => { - deleteFrame(jobID, frame); + // When we call a function, for example: task.annotations.get() + // In the method get we lose the task context + // So, we need return it + this.annotations = { + get: Object.getPrototypeOf(this).annotations.get.bind(this), + put: Object.getPrototypeOf(this).annotations.put.bind(this), + save: Object.getPrototypeOf(this).annotations.save.bind(this), + merge: Object.getPrototypeOf(this).annotations.merge.bind(this), + split: Object.getPrototypeOf(this).annotations.split.bind(this), + group: Object.getPrototypeOf(this).annotations.group.bind(this), + clear: Object.getPrototypeOf(this).annotations.clear.bind(this), + search: Object.getPrototypeOf(this).annotations.search.bind(this), + searchEmpty: Object.getPrototypeOf(this).annotations.searchEmpty.bind(this), + upload: Object.getPrototypeOf(this).annotations.upload.bind(this), + select: Object.getPrototypeOf(this).annotations.select.bind(this), + import: Object.getPrototypeOf(this).annotations.import.bind(this), + export: Object.getPrototypeOf(this).annotations.export.bind(this), + statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this), + hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this), + exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), }; - await redo(); - history.do(HistoryActions.REMOVED_FRAME, async () => { - restoreFrame(jobID, frame); - }, redo, [], frame); - } - - async function restoreFrameWrapper(jobID, frame) { - const history = getHistory(this); - const redo = async () => { - restoreFrame(jobID, frame); + this.actions = { + undo: Object.getPrototypeOf(this).actions.undo.bind(this), + redo: Object.getPrototypeOf(this).actions.redo.bind(this), + freeze: Object.getPrototypeOf(this).actions.freeze.bind(this), + clear: Object.getPrototypeOf(this).actions.clear.bind(this), + get: Object.getPrototypeOf(this).actions.get.bind(this), }; - await redo(); - history.do(HistoryActions.RESTORED_FRAME, async () => { - deleteFrame(jobID, frame); - }, redo, [], frame); - } - - Job.prototype.frames.delete.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } + this.frames = { + get: Object.getPrototypeOf(this).frames.get.bind(this), + delete: Object.getPrototypeOf(this).frames.delete.bind(this), + restore: Object.getPrototypeOf(this).frames.restore.bind(this), + save: Object.getPrototypeOf(this).frames.save.bind(this), + ranges: Object.getPrototypeOf(this).frames.ranges.bind(this), + preview: Object.getPrototypeOf(this).frames.preview.bind(this), + search: Object.getPrototypeOf(this).frames.search.bind(this), + contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this), + }; - if (frame < this.startFrame || frame > this.stopFrame) { - throw new Error('The frame is out of the job'); - } + this.logger = { + log: Object.getPrototypeOf(this).logger.log.bind(this), + }; - await deleteFrameWrapper.call(this, this.id, frame); - }; + this.predictor = { + status: Object.getPrototypeOf(this).predictor.status.bind(this), + predict: Object.getPrototypeOf(this).predictor.predict.bind(this), + }; + } - Job.prototype.frames.restore.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } + /** + * Method updates job data like state, stage or assignee + * @method save + * @memberof module:API.cvat.classes.Job + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async save() { + const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.save); + return result; + } - if (frame < this.startFrame || frame > this.stopFrame) { - throw new Error('The frame is out of the job'); - } + /** + * Method returns a list of issues for a job + * @method issues + * @memberof module:API.cvat.classes.Job + * @returns {module:API.cvat.classes.Issue[]} + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async issues() { + const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.issues); + return result; + } - await restoreFrameWrapper.call(this, this.id, frame); - }; + /** + * Method adds a new issue to a job + * @method openIssue + * @memberof module:API.cvat.classes.Job + * @returns {module:API.cvat.classes.Issue} + * @param {module:API.cvat.classes.Issue} issue + * @param {string} message + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ArgumentError} + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async openIssue(issue, message) { + const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.openIssue, issue, message); + return result; + } - Job.prototype.frames.save.implementation = async function () { - const result = await patchMeta(this.id); + /** + * Method removes all job related data from the client (annotations, history, etc.) + * @method close + * @returns {module:API.cvat.classes.Job} + * @memberof module:API.cvat.classes.Job + * @readonly + * @async + * @instance + * @throws {module:API.cvat.exceptions.PluginError} + */ + async close() { + const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.close); return result; - }; + } +} + +/** + * Class representing a task + * @memberof module:API.cvat.classes + * @extends Session + */ +export class Task extends Session { + /** + * In a fact you need use the constructor only if you want to create a task + * @param {object} initialData - Object which is used for initialization + *
    It can contain keys: + *
  • name + *
  • assignee + *
  • bug_tracker + *
  • labels + *
  • segment_size + *
  • overlap + */ + constructor(initialData) { + super(); + const data = { + id: undefined, + name: undefined, + project_id: null, + status: undefined, + size: undefined, + mode: undefined, + owner: null, + assignee: null, + created_date: undefined, + updated_date: undefined, + bug_tracker: undefined, + subset: undefined, + overlap: undefined, + segment_size: undefined, + image_quality: undefined, + start_frame: undefined, + stop_frame: undefined, + frame_filter: undefined, + data_chunk_size: undefined, + data_compressed_chunk_type: undefined, + data_original_chunk_type: undefined, + deleted_frames: undefined, + use_zip_chunks: undefined, + use_cache: undefined, + copy_data: undefined, + dimension: undefined, + cloud_storage_id: undefined, + sorting_method: undefined, + source_storage: undefined, + target_storage: undefined, + }; - Job.prototype.frames.ranges.implementation = async function () { - const rangesData = await getRanges(this.id); - return rangesData; - }; + const updateTrigger = new FieldUpdateTrigger(); - Job.prototype.frames.preview.implementation = async function () { - if (this.id === null || this.taskId === null) { - return ''; + for (const property in data) { + if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { + data[property] = initialData[property]; + } } - const frameData = await getPreview(this.taskId, this.id); - return frameData; - }; - - Job.prototype.frames.contextImage.implementation = async function (frameId) { - const result = await getContextImage(this.id, frameId); - return result; - }; + if (data.assignee) data.assignee = new User(data.assignee); + if (data.owner) data.owner = new User(data.owner); - Job.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { - if (typeof filters !== 'object') { - throw new ArgumentError('Filters should be an object'); - } + data.labels = []; + data.jobs = []; + data.files = Object.freeze({ + server_files: [], + client_files: [], + remote_files: [], + }); - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); + if (Array.isArray(initialData.labels)) { + data.labels = initialData.labels + .map((labelData) => new Label(labelData)).filter((label) => !label.hasParent); + } + + if (Array.isArray(initialData.segments)) { + for (const segment of initialData.segments) { + if (Array.isArray(segment.jobs)) { + for (const job of segment.jobs) { + const jobInstance = new Job({ + url: job.url, + id: job.id, + assignee: job.assignee, + state: job.state, + stage: job.stage, + start_frame: segment.start_frame, + stop_frame: segment.stop_frame, + // following fields also returned when doing API request /jobs/ + // here we know them from task and append to constructor + task_id: data.id, + project_id: data.project_id, + labels: data.labels, + bug_tracker: data.bug_tracker, + mode: data.mode, + dimension: data.dimension, + data_compressed_chunk_type: data.data_compressed_chunk_type, + data_chunk_size: data.data_chunk_size, + }); + + data.jobs.push(jobInstance); + } + } + } } - if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { - throw new ArgumentError('The start frame is out of the job'); - } + Object.defineProperties( + this, + Object.freeze({ + /** + * @name id + * @type {number} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + id: { + get: () => data.id, + }, + /** + * @name name + * @type {string} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + name: { + get: () => data.name, + set: (value) => { + if (!value.trim().length) { + throw new ArgumentError('Value must not be empty'); + } + updateTrigger.update('name'); + data.name = value; + }, + }, + /** + * @name projectId + * @type {number|null} + * @memberof module:API.cvat.classes.Task + * @instance + */ + projectId: { + get: () => data.project_id, + set: (projectId) => { + if (!Number.isInteger(projectId) || projectId <= 0) { + throw new ArgumentError('Value must be a positive integer'); + } - if (frameTo < this.startFrame || frameTo > this.stopFrame) { - throw new ArgumentError('The stop frame is out of the job'); - } - if (filters.notDeleted) { - return findNotDeletedFrame(this.id, frameFrom, frameTo, filters.offset || 1); - } - return null; - }; + updateTrigger.update('projectId'); + data.project_id = projectId; + }, + }, + /** + * @name status + * @type {module:API.cvat.enums.TaskStatus} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + status: { + get: () => data.status, + }, + /** + * @name size + * @type {number} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + size: { + get: () => data.size, + }, + /** + * @name mode + * @type {TaskMode} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + mode: { + get: () => data.mode, + }, + /** + * Instance of a user who has created the task + * @name owner + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + owner: { + get: () => data.owner, + }, + /** + * Instance of a user who is responsible for the task + * @name assignee + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + assignee: { + get: () => data.assignee, + set: (assignee) => { + if (assignee !== null && !(assignee instanceof User)) { + throw new ArgumentError('Value must be a user instance'); + } + updateTrigger.update('assignee'); + data.assignee = assignee; + }, + }, + /** + * @name createdDate + * @type {string} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + createdDate: { + get: () => data.created_date, + }, + /** + * @name updatedDate + * @type {string} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + updatedDate: { + get: () => data.updated_date, + }, + /** + * @name bugTracker + * @type {string} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + bugTracker: { + get: () => data.bug_tracker, + set: (tracker) => { + if (typeof tracker !== 'string') { + throw new ArgumentError( + `Subset value must be a string. But ${typeof tracker} has been got.`, + ); + } - // TODO: Check filter for annotations - Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { - if (!Array.isArray(filters)) { - throw new ArgumentError('Filters must be an array'); - } + updateTrigger.update('bugTracker'); + data.bug_tracker = tracker; + }, + }, + /** + * @name subset + * @type {string} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exception.ArgumentError} + */ + subset: { + get: () => data.subset, + set: (subset) => { + if (typeof subset !== 'string') { + throw new ArgumentError( + `Subset value must be a string. But ${typeof subset} has been got.`, + ); + } - if (!Number.isInteger(frame)) { - throw new ArgumentError('The frame argument must be an integer'); - } + updateTrigger.update('subset'); + data.subset = subset; + }, + }, + /** + * @name overlap + * @type {number} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + overlap: { + get: () => data.overlap, + set: (overlap) => { + if (!Number.isInteger(overlap) || overlap < 0) { + throw new ArgumentError('Value must be a non negative integer'); + } + data.overlap = overlap; + }, + }, + /** + * @name segmentSize + * @type {number} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + segmentSize: { + get: () => data.segment_size, + set: (segment) => { + if (!Number.isInteger(segment) || segment < 0) { + throw new ArgumentError('Value must be a positive integer'); + } + data.segment_size = segment; + }, + }, + /** + * @name imageQuality + * @type {number} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + imageQuality: { + get: () => data.image_quality, + set: (quality) => { + if (!Number.isInteger(quality) || quality < 0) { + throw new ArgumentError('Value must be a positive integer'); + } + data.image_quality = quality; + }, + }, + /** + * @name useZipChunks + * @type {boolean} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + useZipChunks: { + get: () => data.use_zip_chunks, + set: (useZipChunks) => { + if (typeof useZipChunks !== 'boolean') { + throw new ArgumentError('Value must be a boolean'); + } + data.use_zip_chunks = useZipChunks; + }, + }, + /** + * @name useCache + * @type {boolean} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + useCache: { + get: () => data.use_cache, + set: (useCache) => { + if (typeof useCache !== 'boolean') { + throw new ArgumentError('Value must be a boolean'); + } + data.use_cache = useCache; + }, + }, + /** + * @name copyData + * @type {boolean} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + copyData: { + get: () => data.copy_data, + set: (copyData) => { + if (typeof copyData !== 'boolean') { + throw new ArgumentError('Value must be a boolean'); + } + data.copy_data = copyData; + }, + }, + /** + * @name labels + * @type {module:API.cvat.classes.Label[]} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + labels: { + get: () => data.labels.filter((_label) => !_label.deleted), + set: (labels) => { + if (!Array.isArray(labels)) { + throw new ArgumentError('Value must be an array of Labels'); + } - if (frame < this.startFrame || frame > this.stopFrame) { - throw new ArgumentError(`Frame ${frame} does not exist in the job`); - } + for (const label of labels) { + if (!(label instanceof Label)) { + throw new ArgumentError( + `Each array value must be an instance of Label. ${typeof label} was found`, + ); + } + } - const annotationsData = await getAnnotations(this, frame, allTracks, filters); - const deletedFrames = await getDeletedFrames('job', this.id); - if (frame in deletedFrames) { - return []; - } + const IDs = labels.map((_label) => _label.id); + const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id)); + deletedLabels.forEach((_label) => { + _label.deleted = true; + }); - return annotationsData; - }; + updateTrigger.update('labels'); + data.labels = [...deletedLabels, ...labels]; + }, + }, + /** + * @name jobs + * @type {module:API.cvat.classes.Job[]} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + jobs: { + get: () => [...data.jobs], + }, + /** + * List of files from shared resource or list of cloud storage files + * @name serverFiles + * @type {string[]} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + serverFiles: { + get: () => [...data.files.server_files], + set: (serverFiles) => { + if (!Array.isArray(serverFiles)) { + throw new ArgumentError( + `Value must be an array. But ${typeof serverFiles} has been got.`, + ); + } - Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { - if (!Array.isArray(filters)) { - throw new ArgumentError('Filters must be an array'); - } + for (const value of serverFiles) { + if (typeof value !== 'string') { + throw new ArgumentError( + `Array values must be a string. But ${typeof value} has been got.`, + ); + } + } - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + Array.prototype.push.apply(data.files.server_files, serverFiles); + }, + }, + /** + * List of files from client host + * @name clientFiles + * @type {File[]} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + clientFiles: { + get: () => [...data.files.client_files], + set: (clientFiles) => { + if (!Array.isArray(clientFiles)) { + throw new ArgumentError( + `Value must be an array. But ${typeof clientFiles} has been got.`, + ); + } - if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { - throw new ArgumentError('The start frame is out of the job'); - } + for (const value of clientFiles) { + if (!(value instanceof File)) { + throw new ArgumentError( + `Array values must be a File. But ${value.constructor.name} has been got.`, + ); + } + } - if (frameTo < this.startFrame || frameTo > this.stopFrame) { - throw new ArgumentError('The stop frame is out of the job'); - } + Array.prototype.push.apply(data.files.client_files, clientFiles); + }, + }, + /** + * List of files from remote host + * @name remoteFiles + * @type {File[]} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + remoteFiles: { + get: () => [...data.files.remote_files], + set: (remoteFiles) => { + if (!Array.isArray(remoteFiles)) { + throw new ArgumentError( + `Value must be an array. But ${typeof remoteFiles} has been got.`, + ); + } - const result = searchAnnotations(this, filters, frameFrom, frameTo); - return result; - }; + for (const value of remoteFiles) { + if (typeof value !== 'string') { + throw new ArgumentError( + `Array values must be a string. But ${typeof value} has been got.`, + ); + } + } - Job.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + Array.prototype.push.apply(data.files.remote_files, remoteFiles); + }, + }, + /** + * The first frame of a video to annotation + * @name startFrame + * @type {number} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + startFrame: { + get: () => data.start_frame, + set: (frame) => { + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError('Value must be a not negative integer'); + } + data.start_frame = frame; + }, + }, + /** + * The last frame of a video to annotation + * @name stopFrame + * @type {number} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + stopFrame: { + get: () => data.stop_frame, + set: (frame) => { + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError('Value must be a not negative integer'); + } + data.stop_frame = frame; + }, + }, + /** + * Filter to ignore some frames during task creation + * @name frameFilter + * @type {string} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + frameFilter: { + get: () => data.frame_filter, + set: (filter) => { + if (typeof filter !== 'string') { + throw new ArgumentError( + `Filter value must be a string. But ${typeof filter} has been got.`, + ); + } - if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { - throw new ArgumentError('The start frame is out of the job'); - } + data.frame_filter = filter; + }, + }, + dataChunkSize: { + get: () => data.data_chunk_size, + set: (chunkSize) => { + if (typeof chunkSize !== 'number' || chunkSize < 1) { + throw new ArgumentError( + `Chunk size value must be a positive number. But value ${chunkSize} has been got.`, + ); + } - if (frameTo < this.startFrame || frameTo > this.stopFrame) { - throw new ArgumentError('The stop frame is out of the job'); - } + data.data_chunk_size = chunkSize; + }, + }, + dataChunkType: { + get: () => data.data_compressed_chunk_type, + }, + /** + * @name dimension + * @type {module:API.cvat.enums.DimensionType} + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + */ + dimension: { + get: () => data.dimension, + }, + /** + * @name cloudStorageId + * @type {integer|null} + * @memberof module:API.cvat.classes.Task + * @instance + */ + cloudStorageId: { + get: () => data.cloud_storage_id, + }, + sortingMethod: { + /** + * @name sortingMethod + * @type {module:API.cvat.enums.SortingMethod} + * @memberof module:API.cvat.classes.Task + * @instance + * @readonly + */ + get: () => data.sorting_method, + }, + /** + * Source storage for import resources. + * @name sourceStorage + * @type {module:API.cvat.classes.Storage} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + sourceStorage: { + get: () => ( + new Storage({ + location: data.source_storage?.location || StorageLocation.LOCAL, + cloudStorageId: data.source_storage?.cloud_storage_id, + }) + ), + }, + /** + * Target storage for export resources. + * @name targetStorage + * @type {module:API.cvat.classes.Storage} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + targetStorage: { + get: () => ( + new Storage({ + location: data.target_storage?.location || StorageLocation.LOCAL, + cloudStorageId: data.target_storage?.cloud_storage_id, + }) + ), + }, + _internalData: { + get: () => data, + }, + _updateTrigger: { + get: () => updateTrigger, + }, + }), + ); - const result = searchEmptyFrame(this, frameFrom, frameTo); - return result; - }; + // When we call a function, for example: task.annotations.get() + // In the method get we lose the task context + // So, we need return it + this.annotations = { + get: Object.getPrototypeOf(this).annotations.get.bind(this), + put: Object.getPrototypeOf(this).annotations.put.bind(this), + save: Object.getPrototypeOf(this).annotations.save.bind(this), + merge: Object.getPrototypeOf(this).annotations.merge.bind(this), + split: Object.getPrototypeOf(this).annotations.split.bind(this), + group: Object.getPrototypeOf(this).annotations.group.bind(this), + clear: Object.getPrototypeOf(this).annotations.clear.bind(this), + search: Object.getPrototypeOf(this).annotations.search.bind(this), + searchEmpty: Object.getPrototypeOf(this).annotations.searchEmpty.bind(this), + upload: Object.getPrototypeOf(this).annotations.upload.bind(this), + select: Object.getPrototypeOf(this).annotations.select.bind(this), + import: Object.getPrototypeOf(this).annotations.import.bind(this), + export: Object.getPrototypeOf(this).annotations.export.bind(this), + statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this), + hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this), + exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), + }; - Job.prototype.annotations.save.implementation = async function (onUpdate) { - const result = await saveAnnotations(this, onUpdate); - return result; - }; + this.actions = { + undo: Object.getPrototypeOf(this).actions.undo.bind(this), + redo: Object.getPrototypeOf(this).actions.redo.bind(this), + freeze: Object.getPrototypeOf(this).actions.freeze.bind(this), + clear: Object.getPrototypeOf(this).actions.clear.bind(this), + get: Object.getPrototypeOf(this).actions.get.bind(this), + }; - Job.prototype.annotations.merge.implementation = async function (objectStates) { - const result = await mergeAnnotations(this, objectStates); - return result; - }; + this.frames = { + get: Object.getPrototypeOf(this).frames.get.bind(this), + delete: Object.getPrototypeOf(this).frames.delete.bind(this), + restore: Object.getPrototypeOf(this).frames.restore.bind(this), + save: Object.getPrototypeOf(this).frames.save.bind(this), + ranges: Object.getPrototypeOf(this).frames.ranges.bind(this), + preview: Object.getPrototypeOf(this).frames.preview.bind(this), + contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this), + search: Object.getPrototypeOf(this).frames.search.bind(this), + }; - Job.prototype.annotations.split.implementation = async function (objectState, frame) { - const result = await splitAnnotations(this, objectState, frame); - return result; - }; + this.logger = { + log: Object.getPrototypeOf(this).logger.log.bind(this), + }; - Job.prototype.annotations.group.implementation = async function (objectStates, reset) { - const result = await groupAnnotations(this, objectStates, reset); - return result; - }; + this.predictor = { + status: Object.getPrototypeOf(this).predictor.status.bind(this), + predict: Object.getPrototypeOf(this).predictor.predict.bind(this), + }; + } - Job.prototype.annotations.hasUnsavedChanges.implementation = function () { - const result = hasUnsavedChanges(this); + /** + * Method removes all task related data from the client (annotations, history, etc.) + * @method close + * @returns {module:API.cvat.classes.Task} + * @memberof module:API.cvat.classes.Task + * @readonly + * @async + * @instance + * @throws {module:API.cvat.exceptions.PluginError} + */ + async close() { + const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.close); return result; - }; + } - Job.prototype.annotations.clear.implementation = async function ( - reload, startframe, endframe, delTrackKeyframesOnly, - ) { - const result = await clearAnnotations(this, reload, startframe, endframe, delTrackKeyframesOnly); + /** + * Method updates data of a created task or creates new task from scratch + * @method save + * @returns {module:API.cvat.classes.Task} + * @memberof module:API.cvat.classes.Task + * @param {function} [onUpdate] - the function which is used only if task hasn't + * been created yet. It called in order to notify about creation status. + * It receives the string parameter which is a status message + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async save(onUpdate = () => {}) { + const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.save, onUpdate); return result; - }; + } - Job.prototype.annotations.select.implementation = function (frame, x, y) { - const result = selectObject(this, frame, x, y); + /** + * Method deletes a task from a server + * @method delete + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async delete() { + const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.delete); return result; - }; + } - Job.prototype.annotations.statistics.implementation = function () { - const result = annotationsStatistics(this); + /** + * Method makes a backup of a task + * @method backup + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async backup(targetStorage: Storage, useDefaultSettings: boolean, fileName?: string) { + const result = await PluginRegistry.apiWrapper.call( + this, + Task.prototype.backup, + targetStorage, + useDefaultSettings, + fileName, + ); return result; - }; + } - Job.prototype.annotations.put.implementation = function (objectStates) { - const result = putAnnotations(this, objectStates); + /** + * Method restores a task from a backup + * @method restore + * @memberof module:API.cvat.classes.Task + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + static async restore(storage: Storage, file: File | string) { + const result = await PluginRegistry.apiWrapper.call(this, Task.restore, storage, file); return result; - }; + } +} + +const { + getAnnotations, + putAnnotations, + saveAnnotations, + hasUnsavedChanges, + searchAnnotations, + searchEmptyFrame, + mergeAnnotations, + splitAnnotations, + groupAnnotations, + clearAnnotations, + selectObject, + annotationsStatistics, + importCollection, + exportCollection, + importDataset, + exportDataset, + undoActions, + redoActions, + freezeHistory, + clearActions, + getActions, + closeSession, + getHistory, +} = require('./annotations'); + +buildDuplicatedAPI(Job.prototype); +buildDuplicatedAPI(Task.prototype); + +Job.prototype.save.implementation = async function () { + if (this.id) { + const jobData = this._updateTrigger.getUpdated(this); + if (jobData.assignee) { + jobData.assignee = jobData.assignee.id; + } + + const data = await serverProxy.jobs.save(this.id, jobData); + this._updateTrigger.reset(); + return new Job(data); + } - Job.prototype.annotations.upload.implementation = async function (file, loader) { - const result = await uploadAnnotations(this, file, loader); - return result; - }; + throw new ArgumentError('Could not save job without id'); +}; + +Job.prototype.issues.implementation = async function () { + const result = await serverProxy.issues.get(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}"`); + } - Job.prototype.annotations.import.implementation = function (data) { - const result = importAnnotations(this, data); - return result; - }; + if (frame < this.startFrame || frame > this.stopFrame) { + throw new ArgumentError(`The frame with number ${frame} is out of the job`); + } - Job.prototype.annotations.export.implementation = function () { - const result = exportAnnotations(this); - return result; - }; + const frameData = await getFrame( + this.id, + this.dataChunkSize, + this.dataChunkType, + this.mode, + frame, + this.startFrame, + this.stopFrame, + isPlaying, + step, + this.dimension, + ); + return frameData; +}; + +// must be called with task/job context +async function deleteFrameWrapper(jobID, frame) { + const history = getHistory(this); + const redo = async () => { + deleteFrame(jobID, frame); + }; + + await redo(); + history.do(HistoryActions.REMOVED_FRAME, async () => { + restoreFrame(jobID, frame); + }, redo, [], frame); +} + +async function restoreFrameWrapper(jobID, frame) { + const history = getHistory(this); + const redo = async () => { + restoreFrame(jobID, frame); + }; + + await redo(); + history.do(HistoryActions.RESTORED_FRAME, async () => { + deleteFrame(jobID, frame); + }, redo, [], frame); +} + +Job.prototype.frames.delete.implementation = async function (frame) { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } - Job.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) { - const result = await exportDataset(this, format, customName, saveImages); - return result; - }; + if (frame < this.startFrame || frame > this.stopFrame) { + throw new Error('The frame is out of the job'); + } - Job.prototype.actions.undo.implementation = async function (count) { - const result = await undoActions(this, count); - return result; - }; + await deleteFrameWrapper.call(this, this.id, frame); +}; - Job.prototype.actions.redo.implementation = async function (count) { - const result = await redoActions(this, count); - return result; - }; +Job.prototype.frames.restore.implementation = async function (frame) { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } - Job.prototype.actions.freeze.implementation = function (frozen) { - const result = freezeHistory(this, frozen); - return result; - }; + if (frame < this.startFrame || frame > this.stopFrame) { + throw new Error('The frame is out of the job'); + } - Job.prototype.actions.clear.implementation = function () { - const result = clearActions(this); - return result; - }; + await restoreFrameWrapper.call(this, this.id, frame); +}; - Job.prototype.actions.get.implementation = function () { - const result = getActions(this); - return result; - }; +Job.prototype.frames.save.implementation = async function () { + const result = await patchMeta(this.id); + return result; +}; - Job.prototype.logger.log.implementation = async function (logType, payload, wait) { - const result = await loggerStorage.log(logType, { ...payload, task_id: this.taskId, job_id: this.id }, wait); - return result; - }; +Job.prototype.frames.ranges.implementation = async function () { + const rangesData = await getRanges(this.id); + return rangesData; +}; - Job.prototype.predictor.status.implementation = async function () { - if (!Number.isInteger(this.projectId)) { - throw new DataError('The job must belong to a project to use the feature'); - } +Job.prototype.frames.preview.implementation = async function () { + if (this.id === null || this.taskId === null) { + return ''; + } - const result = await serverProxy.predictor.status(this.projectId); - return { - message: result.message, - progress: result.progress, - projectScore: result.score, - timeRemaining: result.time_remaining, - mediaAmount: result.media_amount, - annotationAmount: result.annotation_amount, - }; - }; + const frameData = await getPreview(this.taskId, this.id); + return frameData; +}; - Job.prototype.predictor.predict.implementation = async function (frame) { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } +Job.prototype.frames.contextImage.implementation = async function (frameId) { + const result = await getContextImage(this.id, frameId); + return result; +}; - if (frame < this.startFrame || frame > this.stopFrame) { - throw new ArgumentError(`The frame with number ${frame} is out of the job`); - } +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(this.projectId)) { - throw new DataError('The job must belong to a project to use the feature'); - } + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - const result = await serverProxy.predictor.predict(this.taskId, frame); - return result; - }; + if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { + throw new ArgumentError('The start frame is out of the job'); + } - Job.prototype.close.implementation = function closeTask() { - clearFrames(this.id); - closeSession(this); - return this; - }; + if (frameTo < this.startFrame || frameTo > this.stopFrame) { + throw new ArgumentError('The stop frame is out of the job'); + } + if (filters.notDeleted) { + return findNotDeletedFrame(this.id, frameFrom, frameTo, filters.offset || 1); + } + return null; +}; - Task.prototype.close.implementation = function closeTask() { - for (const job of this.jobs) { - clearFrames(job.id); - closeSession(job); - } +// TODO: Check filter for annotations +Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { + if (!Array.isArray(filters)) { + throw new ArgumentError('Filters must be an array'); + } - closeSession(this); - return this; - }; + if (!Number.isInteger(frame)) { + throw new ArgumentError('The frame argument must be an integer'); + } - Task.prototype.save.implementation = async function (onUpdate) { - // TODO: Add ability to change an owner and an assignee - 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; - } - if (taskData.labels) { - taskData.labels = this._internalData.labels; - taskData.labels = taskData.labels.map((el) => el.toJSON()); - } + if (frame < this.startFrame || frame > this.stopFrame) { + throw new ArgumentError(`Frame ${frame} does not exist in the job`); + } - const data = await serverProxy.tasks.save(this.id, taskData); - this._updateTrigger.reset(); - return new Task(data); - } + const annotationsData = await getAnnotations(this, frame, allTracks, filters); + const deletedFrames = await getDeletedFrames('job', this.id); + if (frame in deletedFrames) { + return []; + } - const taskSpec = { - name: this.name, - labels: this.labels.map((el) => el.toJSON()), - }; + return annotationsData; +}; - 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; - } +Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { + if (!Array.isArray(filters)) { + throw new ArgumentError('Filters must be an array'); + } - 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, - }; + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - if (typeof this.startFrame !== 'undefined') { - taskDataSpec.start_frame = this.startFrame; - } - if (typeof this.stopFrame !== 'undefined') { - taskDataSpec.stop_frame = this.stopFrame; - } - if (typeof this.frameFilter !== 'undefined') { - taskDataSpec.frame_filter = this.frameFilter; - } - if (typeof this.dataChunkSize !== 'undefined') { - taskDataSpec.chunk_size = this.dataChunkSize; - } - if (typeof this.copyData !== 'undefined') { - taskDataSpec.copy_data = this.copyData; - } - if (typeof this.cloudStorageId !== 'undefined') { - taskDataSpec.cloud_storage_id = this.cloudStorageId; - } + if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { + throw new ArgumentError('The start frame is out of the job'); + } - const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate); - return new Task(task); - }; + if (frameTo < this.startFrame || frameTo > this.stopFrame) { + throw new ArgumentError('The stop frame is out of the job'); + } - Task.prototype.delete.implementation = async function () { - const result = await serverProxy.tasks.delete(this.id); - return result; - }; + const result = searchAnnotations(this, filters, frameFrom, frameTo); + return result; +}; - Task.prototype.export.implementation = async function () { - const result = await serverProxy.tasks.export(this.id); - return result; - }; +Job.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - Task.import.implementation = async function (file) { - // eslint-disable-next-line no-unsanitized/method - const result = await serverProxy.tasks.import(file); - return result; - }; + if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { + throw new ArgumentError('The start frame is out of the job'); + } - 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 (frameTo < this.startFrame || frameTo > this.stopFrame) { + throw new ArgumentError('The stop frame is out of the job'); + } - if (frame >= this.size) { - throw new ArgumentError(`The frame with number ${frame} is out of the task`); - } + const result = searchEmptyFrame(this, frameFrom, frameTo); + return result; +}; + +Job.prototype.annotations.save.implementation = async function (onUpdate) { + const result = await saveAnnotations(this, onUpdate); + return result; +}; + +Job.prototype.annotations.merge.implementation = async function (objectStates) { + const result = await mergeAnnotations(this, objectStates); + return result; +}; + +Job.prototype.annotations.split.implementation = async function (objectState, frame) { + const result = await splitAnnotations(this, objectState, frame); + return result; +}; + +Job.prototype.annotations.group.implementation = async function (objectStates, reset) { + const result = await groupAnnotations(this, objectStates, reset); + return result; +}; + +Job.prototype.annotations.hasUnsavedChanges.implementation = function () { + const result = hasUnsavedChanges(this); + return result; +}; + +Job.prototype.annotations.clear.implementation = async function ( + reload, startframe, endframe, delTrackKeyframesOnly, +) { + const result = await clearAnnotations(this, reload, startframe, endframe, delTrackKeyframesOnly); + return result; +}; + +Job.prototype.annotations.select.implementation = function (frame, x, y) { + const result = selectObject(this, frame, x, y); + return result; +}; + +Job.prototype.annotations.statistics.implementation = function () { + const result = annotationsStatistics(this); + return result; +}; + +Job.prototype.annotations.put.implementation = function (objectStates) { + const result = putAnnotations(this, objectStates); + return result; +}; + +Job.prototype.annotations.upload.implementation = async function ( + format: string, + useDefaultLocation: boolean, + sourceStorage: Storage, + file: File | string, +) { + const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file); + return result; +}; + +Job.prototype.annotations.import.implementation = function (data) { + const result = importCollection(this, data); + return result; +}; + +Job.prototype.annotations.export.implementation = function () { + const result = exportCollection(this); + return result; +}; + +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; +}; + +Job.prototype.actions.undo.implementation = async function (count) { + const result = await undoActions(this, count); + return result; +}; + +Job.prototype.actions.redo.implementation = async function (count) { + const result = await redoActions(this, count); + return result; +}; + +Job.prototype.actions.freeze.implementation = function (frozen) { + const result = freezeHistory(this, frozen); + return result; +}; + +Job.prototype.actions.clear.implementation = function () { + const result = clearActions(this); + return result; +}; + +Job.prototype.actions.get.implementation = function () { + const result = getActions(this); + return result; +}; + +Job.prototype.logger.log.implementation = async function (logType, payload, wait) { + const result = await loggerStorage.log(logType, { ...payload, task_id: this.taskId, job_id: this.id }, wait); + return result; +}; + +Job.prototype.predictor.status.implementation = async function () { + if (!Number.isInteger(this.projectId)) { + throw new DataError('The job must belong to a project to use the feature'); + } - 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, - ); - return result; + const result = await serverProxy.predictor.status(this.projectId); + return { + message: result.message, + progress: result.progress, + projectScore: result.score, + timeRemaining: result.time_remaining, + mediaAmount: result.media_amount, + annotationAmount: result.annotation_amount, }; +}; - Task.prototype.frames.ranges.implementation = async function () { - const rangesData = { - decoded: [], - buffered: [], - }; - for (const job of this.jobs) { - const { decoded, buffered } = await getRanges(job.id); - rangesData.decoded.push(decoded); - rangesData.buffered.push(buffered); - } - return rangesData; - }; +Job.prototype.predictor.predict.implementation = async function (frame) { + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + } - Task.prototype.frames.preview.implementation = async function () { - if (this.id === null) { - return ''; - } + if (frame < this.startFrame || frame > this.stopFrame) { + throw new ArgumentError(`The frame with number ${frame} is out of the job`); + } - const frameData = await getPreview(this.id); - return frameData; - }; + if (!Number.isInteger(this.projectId)) { + throw new DataError('The job must belong to a project to use the feature'); + } - Task.prototype.frames.delete.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } + const result = await serverProxy.predictor.predict(this.taskId, frame); + return result; +}; - if (frame < 0 || frame >= this.size) { - throw new Error('The frame is out of the task'); - } +Job.prototype.close.implementation = function closeTask() { + clearFrames(this.id); + closeSession(this); + return this; +}; - const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; - if (job) { - await deleteFrameWrapper.call(this, job.id, frame); - } - }; +Task.prototype.close.implementation = function closeTask() { + for (const job of this.jobs) { + clearFrames(job.id); + closeSession(job); + } - Task.prototype.frames.restore.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); + closeSession(this); + return this; +}; + +Task.prototype.save.implementation = async function (onUpdate) { + // TODO: Add ability to change an owner and an assignee + 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; } - - if (frame < 0 || frame >= this.size) { - throw new Error('The frame is out of the task'); + if (taskData.labels) { + taskData.labels = this._internalData.labels; + taskData.labels = taskData.labels.map((el) => el.toJSON()); } - const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; - if (job) { - await restoreFrameWrapper.call(this, job.id, frame); - } - }; + const data = await serverProxy.tasks.save(this.id, taskData); + this._updateTrigger.reset(); + return new Task(data); + } - Task.prototype.frames.save.implementation = async function () { - return Promise.all(this.jobs.map((job) => patchMeta(job.id))); + const taskSpec: any = { + name: this.name, + labels: this.labels.map((el) => el.toJSON()), }; - 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'); - } - - 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 (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 (filters.notDeleted) { - for (const job of jobs) { - const result = await findNotDeletedFrame( - job.id, Math.max(frameFrom, job.startFrame), Math.min(frameTo, job.stopFrame), 1, - ); + if (this.targetStorage) { + taskSpec.target_storage = this.targetStorage.toJSON(); + } - if (result !== null) return result; - } - } + if (this.sourceStorage) { + taskSpec.source_storage = this.sourceStorage.toJSON(); + } - return null; + 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, }; - // 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 (typeof this.startFrame !== 'undefined') { + taskDataSpec.start_frame = this.startFrame; + } + if (typeof this.stopFrame !== 'undefined') { + taskDataSpec.stop_frame = this.stopFrame; + } + if (typeof this.frameFilter !== 'undefined') { + taskDataSpec.frame_filter = this.frameFilter; + } + if (typeof this.dataChunkSize !== 'undefined') { + taskDataSpec.chunk_size = this.dataChunkSize; + } + if (typeof this.copyData !== 'undefined') { + taskDataSpec.copy_data = this.copyData; + } + if (typeof this.cloudStorageId !== 'undefined') { + taskDataSpec.cloud_storage_id = this.cloudStorageId; + } - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } + const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate); + return new Task(task); +}; + +Task.prototype.delete.implementation = async function () { + const result = await serverProxy.tasks.delete(this.id); + return result; +}; + +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(`Frame ${frame} does not exist in the task`); - } + if (frame >= this.size) { + throw new ArgumentError(`The frame with number ${frame} is out of the task`); + } - const result = await getAnnotations(this, frame, allTracks, filters); - const deletedFrames = await getDeletedFrames('task', this.id); - if (frame in deletedFrames) { - return []; - } + 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, + ); + return result; +}; + +Task.prototype.frames.ranges.implementation = async function () { + const rangesData = { + decoded: [], + buffered: [], + }; + for (const job of this.jobs) { + const { decoded, buffered } = await getRanges(job.id); + rangesData.decoded.push(decoded); + rangesData.buffered.push(buffered); + } + return rangesData; +}; - return result; - }; +Task.prototype.frames.preview.implementation = async function () { + if (this.id === null) { + return ''; + } - Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { - if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { - throw new ArgumentError('The filters argument must be an array of strings'); - } + const frameData = await getPreview(this.id); + return frameData; +}; - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } +Task.prototype.frames.delete.implementation = async function (frame) { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } - if (frameFrom < 0 || frameFrom >= this.size) { - throw new ArgumentError('The start frame is out of the task'); - } + if (frame < 0 || frame >= this.size) { + throw new Error('The frame is out of the task'); + } - if (frameTo < 0 || frameTo >= this.size) { - throw new ArgumentError('The stop 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); + } +}; - const result = searchAnnotations(this, filters, frameFrom, frameTo); - return result; - }; +Task.prototype.frames.restore.implementation = async function (frame) { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } - Task.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + if (frame < 0 || frame >= this.size) { + throw new Error('The frame is out of the task'); + } - if (frameFrom < 0 || frameFrom >= this.size) { - throw new ArgumentError('The start 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); + } +}; - if (frameTo < 0 || frameTo >= this.size) { - throw new ArgumentError('The stop frame is out of the task'); - } +Task.prototype.frames.save.implementation = async function () { + return Promise.all(this.jobs.map((job) => patchMeta(job.id))); +}; - const result = searchEmptyFrame(this, frameFrom, frameTo); - return result; - }; +Task.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { + if (typeof filters !== 'object') { + throw new ArgumentError('Filters should be an object'); + } - Task.prototype.annotations.save.implementation = async function (onUpdate) { - const result = await saveAnnotations(this, onUpdate); - return result; - }; + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - Task.prototype.annotations.merge.implementation = async function (objectStates) { - const result = await mergeAnnotations(this, objectStates); - return result; - }; + if (frameFrom < 0 || frameFrom > this.size) { + throw new ArgumentError('The start frame is out of the task'); + } - Task.prototype.annotations.split.implementation = async function (objectState, frame) { - const result = await splitAnnotations(this, objectState, frame); - return result; - }; + if (frameTo < 0 || frameTo > this.size) { + throw new ArgumentError('The stop frame is out of the task'); + } - Task.prototype.annotations.group.implementation = async function (objectStates, reset) { - const result = await groupAnnotations(this, objectStates, reset); - return result; - }; + 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.hasUnsavedChanges.implementation = function () { - const result = hasUnsavedChanges(this); - return result; - }; + if (filters.notDeleted) { + for (const job of jobs) { + const result = await findNotDeletedFrame( + job.id, Math.max(frameFrom, job.startFrame), Math.min(frameTo, job.stopFrame), 1, + ); - Task.prototype.annotations.clear.implementation = async function (reload) { - const result = await clearAnnotations(this, reload); - return result; - }; + if (result !== null) return result; + } + } - Task.prototype.annotations.select.implementation = function (frame, x, y) { - const result = selectObject(this, frame, x, y); - return result; - }; + return null; +}; - Task.prototype.annotations.statistics.implementation = function () { - const result = annotationsStatistics(this); - return result; - }; +// 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'); + } - Task.prototype.annotations.put.implementation = function (objectStates) { - const result = putAnnotations(this, objectStates); - return result; - }; + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + } - Task.prototype.annotations.upload.implementation = async function (file, loader) { - const result = await uploadAnnotations(this, file, loader); - return result; - }; + if (frame >= this.size) { + throw new ArgumentError(`Frame ${frame} does not exist in the task`); + } - Task.prototype.annotations.import.implementation = function (data) { - const result = importAnnotations(this, data); - return result; - }; + const result = await getAnnotations(this, frame, allTracks, filters); + const deletedFrames = await getDeletedFrames('task', this.id); + if (frame in deletedFrames) { + return []; + } - Task.prototype.annotations.export.implementation = function () { - const result = exportAnnotations(this); - return result; - }; + return result; +}; - Task.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) { - const result = await exportDataset(this, format, customName, saveImages); - return result; - }; +Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { + if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { + throw new ArgumentError('The filters argument must be an array of strings'); + } - Task.prototype.actions.undo.implementation = function (count) { - const result = undoActions(this, count); - return result; - }; + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - Task.prototype.actions.redo.implementation = function (count) { - const result = redoActions(this, count); - return result; - }; + if (frameFrom < 0 || frameFrom >= this.size) { + throw new ArgumentError('The start frame is out of the task'); + } - Task.prototype.actions.freeze.implementation = function (frozen) { - const result = freezeHistory(this, frozen); - return result; - }; + if (frameTo < 0 || frameTo >= this.size) { + throw new ArgumentError('The stop frame is out of the task'); + } - Task.prototype.actions.clear.implementation = function () { - const result = clearActions(this); - return result; - }; + const result = searchAnnotations(this, filters, frameFrom, frameTo); + return result; +}; - Task.prototype.actions.get.implementation = function () { - const result = getActions(this); - return result; - }; +Task.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { + 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 (logType, payload, wait) { - const result = await loggerStorage.log(logType, { ...payload, 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.predictor.status.implementation = async function () { - if (!Number.isInteger(this.projectId)) { - throw new DataError('The task must belong to a project to use the feature'); - } + if (frameTo < 0 || frameTo >= this.size) { + throw new ArgumentError('The stop frame is out of the task'); + } - const result = await serverProxy.predictor.status(this.projectId); - return { - message: result.message, - progress: result.progress, - projectScore: result.score, - timeRemaining: result.time_remaining, - mediaAmount: result.media_amount, - annotationAmount: result.annotation_amount, - }; + const result = searchEmptyFrame(this, frameFrom, frameTo); + return result; +}; + +Task.prototype.annotations.save.implementation = async function (onUpdate) { + const result = await saveAnnotations(this, onUpdate); + return result; +}; + +Task.prototype.annotations.merge.implementation = async function (objectStates) { + const result = await mergeAnnotations(this, objectStates); + return result; +}; + +Task.prototype.annotations.split.implementation = async function (objectState, frame) { + const result = await splitAnnotations(this, objectState, frame); + return result; +}; + +Task.prototype.annotations.group.implementation = async function (objectStates, reset) { + const result = await groupAnnotations(this, objectStates, reset); + return result; +}; + +Task.prototype.annotations.hasUnsavedChanges.implementation = function () { + const result = hasUnsavedChanges(this); + return result; +}; + +Task.prototype.annotations.clear.implementation = async function (reload) { + const result = await clearAnnotations(this, reload); + return result; +}; + +Task.prototype.annotations.select.implementation = function (frame, x, y) { + const result = selectObject(this, frame, x, y); + return result; +}; + +Task.prototype.annotations.statistics.implementation = function () { + const result = annotationsStatistics(this); + return result; +}; + +Task.prototype.annotations.put.implementation = function (objectStates) { + const result = putAnnotations(this, objectStates); + return result; +}; + +Task.prototype.annotations.upload.implementation = async function ( + format: string, + useDefaultLocation: boolean, + sourceStorage: Storage, + file: File | string, +) { + const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file); + return result; +}; + +Task.prototype.annotations.import.implementation = function (data) { + const result = importCollection(this, data); + return result; +}; + +Task.prototype.annotations.export.implementation = function () { + const result = exportCollection(this); + return result; +}; + +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; +}; + +Task.prototype.actions.undo.implementation = function (count) { + const result = undoActions(this, count); + return result; +}; + +Task.prototype.actions.redo.implementation = function (count) { + const result = redoActions(this, count); + return result; +}; + +Task.prototype.actions.freeze.implementation = function (frozen) { + const result = freezeHistory(this, frozen); + return result; +}; + +Task.prototype.actions.clear.implementation = function () { + const result = clearActions(this); + return result; +}; + +Task.prototype.actions.get.implementation = function () { + const result = getActions(this); + return result; +}; + +Task.prototype.logger.log.implementation = async function (logType, payload, wait) { + const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait); + return result; +}; + +Task.prototype.predictor.status.implementation = async function () { + if (!Number.isInteger(this.projectId)) { + throw new DataError('The task must belong to a project to use the feature'); + } + + const result = await serverProxy.predictor.status(this.projectId); + return { + message: result.message, + progress: result.progress, + projectScore: result.score, + timeRemaining: result.time_remaining, + mediaAmount: result.media_amount, + annotationAmount: result.annotation_amount, }; +}; - Task.prototype.predictor.predict.implementation = async function (frame) { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } +Task.prototype.predictor.predict.implementation = async function (frame) { + 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`); - } + if (frame >= this.size) { + throw new ArgumentError(`The frame with number ${frame} is out of the task`); + } - if (!Number.isInteger(this.projectId)) { - throw new DataError('The task must belong to a project to use the feature'); - } + if (!Number.isInteger(this.projectId)) { + throw new DataError('The task must belong to a project to use the feature'); + } - const result = await serverProxy.predictor.predict(this.id, frame); - return result; - }; -})(); + const result = await serverProxy.predictor.predict(this.id, frame); + return result; +}; diff --git a/cvat-core/src/storage.ts b/cvat-core/src/storage.ts new file mode 100644 index 00000000..9c0e8d32 --- /dev/null +++ b/cvat-core/src/storage.ts @@ -0,0 +1,66 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { StorageLocation } from './enums'; + +export interface StorageData { + location: StorageLocation; + cloudStorageId?: number; +} + +interface StorageJsonData { + location: StorageLocation; + cloud_storage_id?: number; +} + +/** + * Class representing a storage for import and export resources + * @memberof module:API.cvat.classes + * @hideconstructor + */ +export class Storage { + public location: StorageLocation; + public cloudStorageId: number; + + constructor(initialData: StorageData) { + const data: StorageData = { + location: initialData.location, + cloudStorageId: initialData?.cloudStorageId, + }; + + Object.defineProperties( + this, + Object.freeze({ + /** + * @name location + * @type {module:API.cvat.enums.StorageLocation} + * @memberof module:API.cvat.classes.Storage + * @instance + * @readonly + */ + location: { + get: () => data.location, + }, + /** + * @name cloudStorageId + * @type {number} + * @memberof module:API.cvat.classes.Storage + * @instance + * @readonly + */ + cloudStorageId: { + get: () => data.cloudStorageId, + }, + }), + ); + } + toJSON(): StorageJsonData { + return { + location: this.location, + ...(this.cloudStorageId ? { + cloud_storage_id: this.cloudStorageId, + } : {}), + }; + } +} diff --git a/cvat-core/tests/api/annotations.js b/cvat-core/tests/api/annotations.js index de2b1257..e0d074f3 100644 --- a/cvat-core/tests/api/annotations.js +++ b/cvat-core/tests/api/annotations.js @@ -1,17 +1,20 @@ // Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2022 CVAT.ai Corp +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api window.cvat = require('../../src/api'); -const serverProxy = require('../../src/server-proxy'); +const serverProxy = require('../../src/server-proxy').default; // Test cases describe('Feature: get annotations', () => { diff --git a/cvat-core/tests/api/cloud-storages.js b/cvat-core/tests/api/cloud-storages.js index 66ccf11a..a53d34d8 100644 --- a/cvat-core/tests/api/cloud-storages.js +++ b/cvat-core/tests/api/cloud-storages.js @@ -1,11 +1,14 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api diff --git a/cvat-core/tests/api/frames.js b/cvat-core/tests/api/frames.js index e758da70..a707e4ea 100644 --- a/cvat-core/tests/api/frames.js +++ b/cvat-core/tests/api/frames.js @@ -1,11 +1,14 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api diff --git a/cvat-core/tests/api/jobs.js b/cvat-core/tests/api/jobs.js index 568a96f3..3adcdb7f 100644 --- a/cvat-core/tests/api/jobs.js +++ b/cvat-core/tests/api/jobs.js @@ -1,11 +1,14 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api diff --git a/cvat-core/tests/api/object-state.js b/cvat-core/tests/api/object-state.js index bb86279e..0d15ca43 100644 --- a/cvat-core/tests/api/object-state.js +++ b/cvat-core/tests/api/object-state.js @@ -1,11 +1,14 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api diff --git a/cvat-core/tests/api/plugins.js b/cvat-core/tests/api/plugins.js index 373adc48..1f2949ab 100644 --- a/cvat-core/tests/api/plugins.js +++ b/cvat-core/tests/api/plugins.js @@ -1,11 +1,14 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api diff --git a/cvat-core/tests/api/projects.js b/cvat-core/tests/api/projects.js index ea278c24..9c942610 100644 --- a/cvat-core/tests/api/projects.js +++ b/cvat-core/tests/api/projects.js @@ -1,17 +1,20 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api window.cvat = require('../../src/api'); -const { Project } = require('../../src/project'); +const Project = require('../../src/project').default; describe('Feature: get projects', () => { test('get all projects', async () => { diff --git a/cvat-core/tests/api/server.js b/cvat-core/tests/api/server.js index cab2b203..d775d89e 100644 --- a/cvat-core/tests/api/server.js +++ b/cvat-core/tests/api/server.js @@ -1,11 +1,14 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api @@ -23,51 +26,51 @@ describe('Feature: get info about cvat', () => { }); }); -describe('Feature: get share storage info', () => { - test('get files in a root of a share storage', async () => { - const result = await window.cvat.server.share(); - expect(Array.isArray(result)).toBeTruthy(); - expect(result).toHaveLength(5); - }); +// describe('Feature: get share storage info', () => { +// test('get files in a root of a share storage', async () => { +// const result = await window.cvat.server.share(); +// expect(Array.isArray(result)).toBeTruthy(); +// expect(result).toHaveLength(5); +// }); - test('get files in a some dir of a share storage', async () => { - const result = await window.cvat.server.share('images'); - expect(Array.isArray(result)).toBeTruthy(); - expect(result).toHaveLength(8); - }); +// test('get files in a some dir of a share storage', async () => { +// const result = await window.cvat.server.share('images'); +// expect(Array.isArray(result)).toBeTruthy(); +// expect(result).toHaveLength(8); +// }); - test('get files in a some unknown dir of a share storage', async () => { - expect(window.cvat.server.share('Unknown Directory')).rejects.toThrow(window.cvat.exceptions.ServerError); - }); -}); +// test('get files in a some unknown dir of a share storage', async () => { +// expect(window.cvat.server.share('Unknown Directory')).rejects.toThrow(window.cvat.exceptions.ServerError); +// }); +// }); -describe('Feature: get annotation formats', () => { - test('get annotation formats from a server', async () => { - const result = await window.cvat.server.formats(); - expect(result).toBeInstanceOf(AnnotationFormats); - }); -}); +// describe('Feature: get annotation formats', () => { +// test('get annotation formats from a server', async () => { +// const result = await window.cvat.server.formats(); +// expect(result).toBeInstanceOf(AnnotationFormats); +// }); +// }); -describe('Feature: get annotation loaders', () => { - test('get annotation formats from a server', async () => { - const result = await window.cvat.server.formats(); - expect(result).toBeInstanceOf(AnnotationFormats); - const { loaders } = result; - expect(Array.isArray(loaders)).toBeTruthy(); - for (const loader of loaders) { - expect(loader).toBeInstanceOf(Loader); - } - }); -}); +// describe('Feature: get annotation loaders', () => { +// test('get annotation formats from a server', async () => { +// const result = await window.cvat.server.formats(); +// expect(result).toBeInstanceOf(AnnotationFormats); +// const { loaders } = result; +// expect(Array.isArray(loaders)).toBeTruthy(); +// for (const loader of loaders) { +// expect(loader).toBeInstanceOf(Loader); +// } +// }); +// }); -describe('Feature: get annotation dumpers', () => { - test('get annotation formats from a server', async () => { - const result = await window.cvat.server.formats(); - expect(result).toBeInstanceOf(AnnotationFormats); - const { dumpers } = result; - expect(Array.isArray(dumpers)).toBeTruthy(); - for (const dumper of dumpers) { - expect(dumper).toBeInstanceOf(Dumper); - } - }); -}); +// describe('Feature: get annotation dumpers', () => { +// test('get annotation formats from a server', async () => { +// const result = await window.cvat.server.formats(); +// expect(result).toBeInstanceOf(AnnotationFormats); +// const { dumpers } = result; +// expect(Array.isArray(dumpers)).toBeTruthy(); +// for (const dumper of dumpers) { +// expect(dumper).toBeInstanceOf(Dumper); +// } +// }); +// }); diff --git a/cvat-core/tests/api/tasks.js b/cvat-core/tests/api/tasks.js index bbb990c8..7321824b 100644 --- a/cvat-core/tests/api/tasks.js +++ b/cvat-core/tests/api/tasks.js @@ -1,11 +1,14 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api diff --git a/cvat-core/tests/api/user.js b/cvat-core/tests/api/user.js index 14623b77..694aa61e 100644 --- a/cvat-core/tests/api/user.js +++ b/cvat-core/tests/api/user.js @@ -1,11 +1,14 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT // Setup mock for a server jest.mock('../../src/server-proxy', () => { - const mock = require('../mocks/server-proxy.mock'); - return mock; + return { + __esModule: true, + default: require('../mocks/server-proxy.mock'), + }; }); // Initialize api diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 74411692..3eeb9d96 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.41.5", + "version": "1.42.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index b404333f..58485007 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -50,7 +50,7 @@ function getStore(): Store { return store; } -function receiveAnnotationsParameters(): AnnotationsParameters { +export function receiveAnnotationsParameters(): AnnotationsParameters { if (store === null) { store = getCVATStore(); } @@ -89,7 +89,7 @@ export function computeZRange(states: any[]): number[] { return [minZ, maxZ]; } -async function jobInfoGenerator(job: any): Promise> { +export async function jobInfoGenerator(job: any): Promise> { const { total } = await job.annotations.statistics(); return { 'frame count': job.stopFrame - job.startFrame + 1, @@ -350,74 +350,6 @@ export function removeAnnotationsAsync( }; } -export function uploadJobAnnotationsAsync(job: any, loader: any, file: File): ThunkAction { - return async (dispatch: ActionCreator): Promise => { - try { - const state: CombinedState = getStore().getState(); - const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); - - if (state.tasks.activities.loads[job.taskId]) { - throw Error('Annotations is being uploaded for the task'); - } - if (state.annotation.activities.loads[job.id]) { - throw Error('Only one uploading of annotations for a job allowed at the same time'); - } - - dispatch({ - type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS, - payload: { - job, - loader, - }, - }); - - const frame = state.annotation.player.frame.number; - await job.annotations.upload(file, loader); - - await job.logger.log(LogType.uploadAnnotations, { - ...(await jobInfoGenerator(job)), - }); - - await job.annotations.clear(true); - await job.actions.clear(); - const history = await job.actions.get(); - - // One more update to escape some problems - // in canvas when shape with the same - // clientID has different type (polygon, rectangle) for example - dispatch({ - type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS, - payload: { - job, - states: [], - history, - }, - }); - - const states = await job.annotations.get(frame, showAllInterpolationTracks, filters); - - setTimeout(() => { - dispatch({ - type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS, - payload: { - history, - job, - states, - }, - }); - }); - } catch (error) { - dispatch({ - type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_FAILED, - payload: { - job, - error, - }, - }); - } - }; -} - export function collectStatisticsAsync(sessionInstance: any): ThunkAction { return async (dispatch: ActionCreator): Promise => { try { diff --git a/cvat-ui/src/actions/export-actions.ts b/cvat-ui/src/actions/export-actions.ts index d12aa37e..fe38dc73 100644 --- a/cvat-ui/src/actions/export-actions.ts +++ b/cvat-ui/src/actions/export-actions.ts @@ -1,51 +1,130 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; +import { getCore, Storage } from 'cvat-core-wrapper'; + +const core = getCore(); + export enum ExportActionTypes { - OPEN_EXPORT_MODAL = 'OPEN_EXPORT_MODAL', - CLOSE_EXPORT_MODAL = 'CLOSE_EXPORT_MODAL', + OPEN_EXPORT_DATASET_MODAL = 'OPEN_EXPORT_DATASET_MODAL', + CLOSE_EXPORT_DATASET_MODAL = 'CLOSE_EXPORT_DATASET_MODAL', EXPORT_DATASET = 'EXPORT_DATASET', EXPORT_DATASET_SUCCESS = 'EXPORT_DATASET_SUCCESS', EXPORT_DATASET_FAILED = 'EXPORT_DATASET_FAILED', + OPEN_EXPORT_BACKUP_MODAL = 'OPEN_EXPORT_BACKUP_MODAL', + CLOSE_EXPORT_BACKUP_MODAL = 'CLOSE_EXPORT_BACKUP_MODAL', + EXPORT_BACKUP = 'EXPORT_BACKUP', + EXPORT_BACKUP_SUCCESS = 'EXPORT_BACKUP_SUCCESS', + EXPORT_BACKUP_FAILED = 'EXPORT_BACKUP_FAILED', } export const exportActions = { - openExportModal: (instance: any) => createAction(ExportActionTypes.OPEN_EXPORT_MODAL, { instance }), - closeExportModal: () => createAction(ExportActionTypes.CLOSE_EXPORT_MODAL), + openExportDatasetModal: (instance: any) => ( + createAction(ExportActionTypes.OPEN_EXPORT_DATASET_MODAL, { instance }) + ), + closeExportDatasetModal: (instance: any) => ( + createAction(ExportActionTypes.CLOSE_EXPORT_DATASET_MODAL, { instance }) + ), exportDataset: (instance: any, format: string) => ( createAction(ExportActionTypes.EXPORT_DATASET, { instance, format }) ), - exportDatasetSuccess: (instance: any, format: string) => ( - createAction(ExportActionTypes.EXPORT_DATASET_SUCCESS, { instance, format }) + exportDatasetSuccess: ( + instance: any, + instanceType: 'project' | 'task' | 'job', + format: string, + isLocal: boolean, + resource: 'Dataset' | 'Annotations', + ) => ( + createAction(ExportActionTypes.EXPORT_DATASET_SUCCESS, { + instance, + instanceType, + format, + isLocal, + resource, + }) ), - exportDatasetFailed: (instance: any, format: string, error: any) => ( + exportDatasetFailed: (instance: any, instanceType: 'project' | 'task' | 'job', format: string, error: any) => ( createAction(ExportActionTypes.EXPORT_DATASET_FAILED, { instance, + instanceType, format, error, }) ), + openExportBackupModal: (instance: any) => ( + createAction(ExportActionTypes.OPEN_EXPORT_BACKUP_MODAL, { instance }) + ), + closeExportBackupModal: (instance: any) => ( + createAction(ExportActionTypes.CLOSE_EXPORT_BACKUP_MODAL, { instance }) + ), + exportBackup: (instance: any) => ( + createAction(ExportActionTypes.EXPORT_BACKUP, { instance }) + ), + exportBackupSuccess: (instance: any, instanceType: 'task' | 'project', isLocal: boolean) => ( + createAction(ExportActionTypes.EXPORT_BACKUP_SUCCESS, { instance, instanceType, isLocal }) + ), + exportBackupFailed: (instance: any, instanceType: 'task' | 'project', error: any) => ( + createAction(ExportActionTypes.EXPORT_BACKUP_FAILED, { instance, instanceType, error }) + ), }; export const exportDatasetAsync = ( instance: any, format: string, - name: string, saveImages: boolean, + useDefaultSettings: boolean, + targetStorage: Storage, + name?: string, ): ThunkAction => async (dispatch) => { dispatch(exportActions.exportDataset(instance, format)); + let instanceType: 'project' | 'task' | 'job'; + if (instance instanceof core.classes.Project) { + instanceType = 'project'; + } else if (instance instanceof core.classes.Task) { + instanceType = 'task'; + } else { + instanceType = 'job'; + } + + try { + const result = await instance.annotations + .exportDataset(format, saveImages, useDefaultSettings, targetStorage, name); + if (result) { + const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement; + downloadAnchor.href = result; + downloadAnchor.click(); + } + const resource = saveImages ? 'Dataset' : 'Annotations'; + dispatch(exportActions.exportDatasetSuccess(instance, instanceType, format, !!result, resource)); + } catch (error) { + dispatch(exportActions.exportDatasetFailed(instance, instanceType, format, error)); + } +}; + +export const exportBackupAsync = ( + instance: any, + targetStorage: Storage, + useDefaultSetting: boolean, + fileName?: string, +): ThunkAction => async (dispatch) => { + dispatch(exportActions.exportBackup(instance)); + const instanceType = (instance instanceof core.classes.Project) ? 'project' : 'task'; + try { - const url = await instance.annotations.exportDataset(format, saveImages, name); - const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement; - downloadAnchor.href = url; - downloadAnchor.click(); - dispatch(exportActions.exportDatasetSuccess(instance, format)); + const result = await instance.backup(targetStorage, useDefaultSetting, fileName); + if (result) { + const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement; + downloadAnchor.href = result; + downloadAnchor.click(); + } + dispatch(exportActions.exportBackupSuccess(instance, instanceType, !!result)); } catch (error) { - dispatch(exportActions.exportDatasetFailed(instance, format, error)); + dispatch(exportActions.exportBackupFailed(instance, instanceType, error as Error)); } }; diff --git a/cvat-ui/src/actions/import-actions.ts b/cvat-ui/src/actions/import-actions.ts index 94fcd562..6372850b 100644 --- a/cvat-ui/src/actions/import-actions.ts +++ b/cvat-ui/src/actions/import-actions.ts @@ -1,58 +1,167 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { createAction, ActionUnion, ThunkAction } from 'utils/redux'; import { CombinedState } from 'reducers'; +import { getCore, Storage } from 'cvat-core-wrapper'; +import { LogType } from 'cvat-logger'; import { getProjectsAsync } from './projects-actions'; +import { jobInfoGenerator, receiveAnnotationsParameters, AnnotationActionTypes } from './annotation-actions'; + +const core = getCore(); export enum ImportActionTypes { - OPEN_IMPORT_MODAL = 'OPEN_IMPORT_MODAL', - CLOSE_IMPORT_MODAL = 'CLOSE_IMPORT_MODAL', + OPEN_IMPORT_DATASET_MODAL = 'OPEN_IMPORT_DATASET_MODAL', + CLOSE_IMPORT_DATASET_MODAL = 'CLOSE_IMPORT_DATASET_MODAL', IMPORT_DATASET = 'IMPORT_DATASET', IMPORT_DATASET_SUCCESS = 'IMPORT_DATASET_SUCCESS', IMPORT_DATASET_FAILED = 'IMPORT_DATASET_FAILED', IMPORT_DATASET_UPDATE_STATUS = 'IMPORT_DATASET_UPDATE_STATUS', + OPEN_IMPORT_BACKUP_MODAL = 'OPEN_IMPORT_BACKUP_MODAL', + CLOSE_IMPORT_BACKUP_MODAL = 'CLOSE_IMPORT_BACKUP_MODAL', + IMPORT_BACKUP = 'IMPORT_BACKUP', + IMPORT_BACKUP_SUCCESS = 'IMPORT_BACKUP_SUCCESS', + IMPORT_BACKUP_FAILED = 'IMPORT_BACKUP_FAILED', } export const importActions = { - openImportModal: (instance: any) => createAction(ImportActionTypes.OPEN_IMPORT_MODAL, { instance }), - closeImportModal: () => createAction(ImportActionTypes.CLOSE_IMPORT_MODAL), - importDataset: (projectId: number) => ( - createAction(ImportActionTypes.IMPORT_DATASET, { id: projectId }) + openImportDatasetModal: (instance: any) => ( + createAction(ImportActionTypes.OPEN_IMPORT_DATASET_MODAL, { instance }) + ), + closeImportDatasetModal: (instance: any) => ( + createAction(ImportActionTypes.CLOSE_IMPORT_DATASET_MODAL, { instance }) + ), + importDataset: (instance: any, format: string) => ( + createAction(ImportActionTypes.IMPORT_DATASET, { instance, format }) ), - importDatasetSuccess: () => ( - createAction(ImportActionTypes.IMPORT_DATASET_SUCCESS) + importDatasetSuccess: (instance: any, resource: 'dataset' | 'annotation') => ( + createAction(ImportActionTypes.IMPORT_DATASET_SUCCESS, { instance, resource }) ), - importDatasetFailed: (instance: any, error: any) => ( + importDatasetFailed: (instance: any, resource: 'dataset' | 'annotation', error: any) => ( createAction(ImportActionTypes.IMPORT_DATASET_FAILED, { instance, + resource, error, }) ), - importDatasetUpdateStatus: (progress: number, status: string) => ( - createAction(ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS, { progress, status }) + importDatasetUpdateStatus: (instance: any, progress: number, status: string) => ( + createAction(ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS, { instance, progress, status }) + ), + openImportBackupModal: (instanceType: 'project' | 'task') => ( + createAction(ImportActionTypes.OPEN_IMPORT_BACKUP_MODAL, { instanceType }) + ), + closeImportBackupModal: (instanceType: 'project' | 'task') => ( + createAction(ImportActionTypes.CLOSE_IMPORT_BACKUP_MODAL, { instanceType }) + ), + importBackup: () => createAction(ImportActionTypes.IMPORT_BACKUP), + importBackupSuccess: (instanceId: number, instanceType: 'project' | 'task') => ( + createAction(ImportActionTypes.IMPORT_BACKUP_SUCCESS, { instanceId, instanceType }) + ), + importBackupFailed: (instanceType: 'project' | 'task', error: any) => ( + createAction(ImportActionTypes.IMPORT_BACKUP_FAILED, { instanceType, error }) ), }; -export const importDatasetAsync = (instance: any, format: string, file: File): ThunkAction => ( +export const importDatasetAsync = ( + instance: any, + format: string, + useDefaultSettings: boolean, + sourceStorage: Storage, + file: File | string, +): ThunkAction => ( async (dispatch, getState) => { + const resource = instance instanceof core.classes.Project ? 'dataset' : 'annotation'; + try { const state: CombinedState = getState(); - if (state.import.importingId !== null) { - throw Error('Only one importing of dataset allowed at the same time'); + + if (instance instanceof core.classes.Project) { + if (state.import.projects.dataset.current?.[instance.id]) { + throw Error('Only one importing of annotation/dataset allowed at the same time'); + } + dispatch(importActions.importDataset(instance, format)); + await instance.annotations + .importDataset(format, useDefaultSettings, sourceStorage, file, + (message: string, progress: number) => ( + dispatch(importActions.importDatasetUpdateStatus( + instance, Math.floor(progress * 100), message, + )) + )); + } else if (instance instanceof core.classes.Task) { + if (state.import.tasks.dataset.current?.[instance.id]) { + throw Error('Only one importing of annotation/dataset allowed at the same time'); + } + dispatch(importActions.importDataset(instance, format)); + await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file); + } else { // job + if (state.import.tasks.dataset.current?.[instance.taskId]) { + throw Error('Annotations is being uploaded for the task'); + } + if (state.import.jobs.dataset.current?.[instance.id]) { + throw Error('Only one uploading of annotations for a job allowed at the same time'); + } + const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); + + dispatch(importActions.importDataset(instance, format)); + + const frame = state.annotation.player.frame.number; + await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file); + + await instance.logger.log(LogType.uploadAnnotations, { + ...(await jobInfoGenerator(instance)), + }); + + await instance.annotations.clear(true); + await instance.actions.clear(); + const history = await instance.actions.get(); + + // One more update to escape some problems + // in canvas when shape with the same + // clientID has different type (polygon, rectangle) for example + dispatch({ + type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS, + payload: { + states: [], + history, + }, + }); + + const states = await instance.annotations.get(frame, showAllInterpolationTracks, filters); + + setTimeout(() => { + dispatch({ + type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS, + payload: { + history, + states, + }, + }); + }); } - dispatch(importActions.importDataset(instance.id)); - await instance.annotations.importDataset(format, file, (message: string, progress: number) => ( - dispatch(importActions.importDatasetUpdateStatus(Math.floor(progress * 100), message)) - )); } catch (error) { - dispatch(importActions.importDatasetFailed(instance, error)); + dispatch(importActions.importDatasetFailed(instance, resource, error)); return; } - dispatch(importActions.importDatasetSuccess()); - dispatch(getProjectsAsync({ id: instance.id }, getState().projects.tasksGettingQuery)); + dispatch(importActions.importDatasetSuccess(instance, resource)); + if (instance instanceof core.classes.Project) { + dispatch(getProjectsAsync({ id: instance.id }, getState().projects.tasksGettingQuery)); + } + } +); + +export const importBackupAsync = (instanceType: 'project' | 'task', storage: Storage, file: File | string): ThunkAction => ( + async (dispatch) => { + dispatch(importActions.importBackup()); + try { + const inctanceClass = (instanceType === 'task') ? core.classes.Task : core.classes.Project; + const instance = await inctanceClass.restore(storage, file); + dispatch(importActions.importBackupSuccess(instance.id, instanceType)); + } catch (error) { + dispatch(importActions.importBackupFailed(instanceType, error)); + } } ); diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index f06da0f1..51a90e71 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -28,12 +29,6 @@ export enum ProjectsActionTypes { DELETE_PROJECT = 'DELETE_PROJECT', DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS', DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED', - BACKUP_PROJECT = 'BACKUP_PROJECT', - BACKUP_PROJECT_SUCCESS = 'BACKUP_PROJECT_SUCCESS', - BACKUP_PROJECT_FAILED = 'BACKUP_PROJECT_FAILED', - RESTORE_PROJECT = 'IMPORT_PROJECT', - RESTORE_PROJECT_SUCCESS = 'IMPORT_PROJECT_SUCCESS', - RESTORE_PROJECT_FAILED = 'IMPORT_PROJECT_FAILED', } // prettier-ignore @@ -63,20 +58,6 @@ const projectActions = { deleteProjectFailed: (projectId: number, error: any) => ( createAction(ProjectsActionTypes.DELETE_PROJECT_FAILED, { projectId, error }) ), - backupProject: (projectId: number) => createAction(ProjectsActionTypes.BACKUP_PROJECT, { projectId }), - backupProjectSuccess: (projectID: number) => ( - createAction(ProjectsActionTypes.BACKUP_PROJECT_SUCCESS, { projectID }) - ), - backupProjectFailed: (projectID: number, error: any) => ( - createAction(ProjectsActionTypes.BACKUP_PROJECT_FAILED, { projectId: projectID, error }) - ), - restoreProject: () => createAction(ProjectsActionTypes.RESTORE_PROJECT), - restoreProjectSuccess: (projectID: number) => ( - createAction(ProjectsActionTypes.RESTORE_PROJECT_SUCCESS, { projectID }) - ), - restoreProjectFailed: (error: any) => ( - createAction(ProjectsActionTypes.RESTORE_PROJECT_FAILED, { error }) - ), }; export type ProjectActions = ActionUnion; @@ -190,31 +171,3 @@ export function deleteProjectAsync(projectInstance: any): ThunkAction { } }; } - -export function restoreProjectAsync(file: File): ThunkAction { - return async (dispatch: ActionCreator): Promise => { - dispatch(projectActions.restoreProject()); - try { - const projectInstance = await cvat.classes.Project.restore(file); - dispatch(projectActions.restoreProjectSuccess(projectInstance)); - } catch (error) { - dispatch(projectActions.restoreProjectFailed(error)); - } - }; -} - -export function backupProjectAsync(projectInstance: any): ThunkAction { - return async (dispatch: ActionCreator): Promise => { - dispatch(projectActions.backupProject(projectInstance.id)); - - try { - const url = await projectInstance.backup(); - const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement; - downloadAnchor.href = url; - downloadAnchor.click(); - dispatch(projectActions.backupProjectSuccess(projectInstance.id)); - } catch (error) { - dispatch(projectActions.backupProjectFailed(projectInstance.id, error)); - } - }; -} diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index cb966775..ace911b3 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -1,12 +1,14 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; -import { TasksQuery, CombinedState, Indexable } from 'reducers'; -import { getCVATStore } from 'cvat-store'; -import { getCore } from 'cvat-core-wrapper'; +import { + TasksQuery, CombinedState, Indexable, StorageLocation, +} from 'reducers'; +import { getCore, Storage } from 'cvat-core-wrapper'; import { getInferenceStatusAsync } from './models-actions'; const cvat = getCore(); @@ -15,9 +17,6 @@ export enum TasksActionTypes { GET_TASKS = 'GET_TASKS', GET_TASKS_SUCCESS = 'GET_TASKS_SUCCESS', GET_TASKS_FAILED = 'GET_TASKS_FAILED', - LOAD_ANNOTATIONS = 'LOAD_ANNOTATIONS', - LOAD_ANNOTATIONS_SUCCESS = 'LOAD_ANNOTATIONS_SUCCESS', - LOAD_ANNOTATIONS_FAILED = 'LOAD_ANNOTATIONS_FAILED', DELETE_TASK = 'DELETE_TASK', DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS', DELETE_TASK_FAILED = 'DELETE_TASK_FAILED', @@ -32,12 +31,6 @@ export enum TasksActionTypes { UPDATE_JOB_SUCCESS = 'UPDATE_JOB_SUCCESS', UPDATE_JOB_FAILED = 'UPDATE_JOB_FAILED', HIDE_EMPTY_TASKS = 'HIDE_EMPTY_TASKS', - EXPORT_TASK = 'EXPORT_TASK', - EXPORT_TASK_SUCCESS = 'EXPORT_TASK_SUCCESS', - EXPORT_TASK_FAILED = 'EXPORT_TASK_FAILED', - IMPORT_TASK = 'IMPORT_TASK', - IMPORT_TASK_SUCCESS = 'IMPORT_TASK_SUCCESS', - IMPORT_TASK_FAILED = 'IMPORT_TASK_FAILED', SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE', } @@ -103,157 +96,6 @@ export function getTasksAsync(query: TasksQuery, updateQuery = true): ThunkActio }; } -function loadAnnotations(task: any, loader: any): AnyAction { - const action = { - type: TasksActionTypes.LOAD_ANNOTATIONS, - payload: { - task, - loader, - }, - }; - - return action; -} - -function loadAnnotationsSuccess(task: any): AnyAction { - const action = { - type: TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS, - payload: { - task, - }, - }; - - return action; -} - -function loadAnnotationsFailed(task: any, error: any): AnyAction { - const action = { - type: TasksActionTypes.LOAD_ANNOTATIONS_FAILED, - payload: { - task, - error, - }, - }; - - return action; -} - -export function loadAnnotationsAsync( - task: any, - loader: any, - file: File, -): ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - const store = getCVATStore(); - const state: CombinedState = store.getState(); - if (state.tasks.activities.loads[task.id]) { - throw Error('Only one loading of annotations for a task allowed at the same time'); - } - dispatch(loadAnnotations(task, loader)); - await task.annotations.upload(file, loader); - } catch (error) { - dispatch(loadAnnotationsFailed(task, error)); - return; - } - - dispatch(loadAnnotationsSuccess(task)); - }; -} - -function importTask(): AnyAction { - const action = { - type: TasksActionTypes.IMPORT_TASK, - payload: {}, - }; - - return action; -} - -function importTaskSuccess(task: any): AnyAction { - const action = { - type: TasksActionTypes.IMPORT_TASK_SUCCESS, - payload: { - task, - }, - }; - - return action; -} - -function importTaskFailed(error: any): AnyAction { - const action = { - type: TasksActionTypes.IMPORT_TASK_FAILED, - payload: { - error, - }, - }; - - return action; -} - -export function importTaskAsync(file: File): ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - dispatch(importTask()); - const taskInstance = await cvat.classes.Task.import(file); - dispatch(importTaskSuccess(taskInstance)); - } catch (error) { - dispatch(importTaskFailed(error)); - } - }; -} - -function exportTask(taskID: number): AnyAction { - const action = { - type: TasksActionTypes.EXPORT_TASK, - payload: { - taskID, - }, - }; - - return action; -} - -function exportTaskSuccess(taskID: number): AnyAction { - const action = { - type: TasksActionTypes.EXPORT_TASK_SUCCESS, - payload: { - taskID, - }, - }; - - return action; -} - -function exportTaskFailed(taskID: number, error: Error): AnyAction { - const action = { - type: TasksActionTypes.EXPORT_TASK_FAILED, - payload: { - taskID, - error, - }, - }; - - return action; -} - -export function exportTaskAsync(taskInstance: any): ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - dispatch(exportTask(taskInstance.id)); - - try { - const url = await taskInstance.export(); - const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement; - downloadAnchor.href = url; - downloadAnchor.click(); - dispatch(exportTaskSuccess(taskInstance.id)); - } catch (error) { - dispatch(exportTaskFailed(taskInstance.id, error as Error)); - } - }; -} - function deleteTask(taskID: number): AnyAction { const action = { type: TasksActionTypes.DELETE_TASK, @@ -353,6 +195,8 @@ export function createTaskAsync(data: any): ThunkAction, {}, {}, A use_zip_chunks: data.advanced.useZipChunks, use_cache: data.advanced.useCache, sorting_method: data.advanced.sortingMethod, + source_storage: new Storage(data.advanced.sourceStorage || { location: StorageLocation.LOCAL }).toJSON(), + target_storage: new Storage(data.advanced.targetStorage || { location: StorageLocation.LOCAL }).toJSON(), }; if (data.projectId) { diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index 0282aedf..272393e9 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -9,8 +10,6 @@ import Modal from 'antd/lib/modal'; import { LoadingOutlined } from '@ant-design/icons'; // eslint-disable-next-line import/no-extraneous-dependencies import { MenuInfo } from 'rc-menu/lib/interface'; - -import LoadSubmenu from './load-submenu'; import { DimensionType } from '../../reducers'; interface Props { @@ -19,12 +18,10 @@ interface Props { bugTracker: string; loaders: any[]; dumpers: any[]; - loadActivity: string | null; inferenceIsActive: boolean; taskDimension: DimensionType; + backupIsActive: boolean; onClickMenu: (params: MenuInfo) => void; - onUploadAnnotations: (format: string, file: File) => void; - exportIsActive: boolean; } export enum Actions { @@ -34,7 +31,7 @@ export enum Actions { RUN_AUTO_ANNOTATION = 'run_auto_annotation', MOVE_TASK_TO_PROJECT = 'move_task_to_project', OPEN_BUG_TRACKER = 'open_bug_tracker', - EXPORT_TASK = 'export_task', + BACKUP_TASK = 'backup_task', } function ActionsMenuComponent(props: Props): JSX.Element { @@ -42,12 +39,8 @@ function ActionsMenuComponent(props: Props): JSX.Element { taskID, bugTracker, inferenceIsActive, - loaders, + backupIsActive, onClickMenu, - onUploadAnnotations, - loadActivity, - taskDimension, - exportIsActive, } = props; const onClickMenuWrapper = useCallback( @@ -79,38 +72,16 @@ function ActionsMenuComponent(props: Props): JSX.Element { return ( - {LoadSubmenu({ - loaders, - loadActivity, - onFileUpload: (format: string, file: File): void => { - if (file) { - Modal.confirm({ - title: 'Current annotation will be lost', - content: 'You are going to upload new annotations to this task. Continue?', - className: 'cvat-modal-content-load-task-annotation', - onOk: () => { - onUploadAnnotations(format, file); - }, - okButtonProps: { - type: 'primary', - danger: true, - }, - okText: 'Update', - }); - } - }, - menuKey: Actions.LOAD_TASK_ANNO, - taskDimension, - })} + Upload annotations Export task dataset {!!bugTracker && Open bug tracker} Automatic annotation } + key={Actions.BACKUP_TASK} + disabled={backupIsActive} + icon={backupIsActive && } > Backup Task diff --git a/cvat-ui/src/components/actions-menu/load-submenu.tsx b/cvat-ui/src/components/actions-menu/load-submenu.tsx deleted file mode 100644 index feda16b5..00000000 --- a/cvat-ui/src/components/actions-menu/load-submenu.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (C) 2020-2022 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import React from 'react'; -import Menu from 'antd/lib/menu'; -import Upload from 'antd/lib/upload'; -import Button from 'antd/lib/button'; -import Text from 'antd/lib/typography/Text'; -import { UploadOutlined, LoadingOutlined } from '@ant-design/icons'; -import { DimensionType } from '../../reducers'; - -interface Props { - menuKey: string; - loaders: any[]; - loadActivity: string | null; - onFileUpload(format: string, file: File): void; - taskDimension: DimensionType; -} - -export default function LoadSubmenu(props: Props): JSX.Element { - const { - menuKey, loaders, loadActivity, onFileUpload, taskDimension, - } = props; - - return ( - - {loaders - .sort((a: any, b: any) => a.name.localeCompare(b.name)) - .filter((loader: any): boolean => loader.dimension === taskDimension) - .map( - (loader: any): JSX.Element => { - const accept = loader.format - .split(',') - .map((x: string) => `.${x.trimStart()}`) - .join(', '); // add '.' to each extension in a list - const pending = loadActivity === loader.name; - const disabled = !loader.enabled || !!loadActivity; - const format = loader.name; - return ( - - { - onFileUpload(format, file); - return false; - }} - > - - - - ); - }, - )} - - ); -} diff --git a/cvat-ui/src/components/actions-menu/styles.scss b/cvat-ui/src/components/actions-menu/styles.scss index 25240c9c..1d05bb42 100644 --- a/cvat-ui/src/components/actions-menu/styles.scss +++ b/cvat-ui/src/components/actions-menu/styles.scss @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -17,7 +18,6 @@ } } -.cvat-menu-load-submenu-item, .cvat-menu-dump-submenu-item, .cvat-menu-export-submenu-item { > span[role='img'] { @@ -29,28 +29,12 @@ } } -.ant-menu-item.cvat-menu-load-submenu-item { - margin: 0; - padding: 0; - - > span > .ant-upload { - width: 100%; - height: 100%; - - > span > button { - width: 100%; - height: 100%; - text-align: left; - } - } -} - .cvat-menu-icon { font-size: 16px; margin-left: 8px; align-self: center; } -#cvat-export-task-loading { +#cvat-backup-task-loading { margin-left: 10; } diff --git a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx index b9cff4e0..75d60814 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -14,9 +15,7 @@ import Collapse from 'antd/lib/collapse'; // eslint-disable-next-line import/no-extraneous-dependencies import { MenuInfo } from 'rc-menu/lib/interface'; - import CVATTooltip from 'components/common/cvat-tooltip'; -import LoadSubmenu from 'components/actions-menu/load-submenu'; import { getCore } from 'cvat-core-wrapper'; import { JobStage } from 'reducers'; @@ -24,12 +23,8 @@ const core = getCore(); interface Props { taskMode: string; - loaders: any[]; - dumpers: any[]; - loadActivity: string | null; jobInstance: any; onClickMenu(params: MenuInfo): void; - onUploadAnnotations(format: string, file: File): void; stopFrame: number; removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean): void; setForceExitAnnotationFlag(forceExit: boolean): void; @@ -38,7 +33,7 @@ interface Props { export enum Actions { LOAD_JOB_ANNO = 'load_job_anno', - EXPORT_TASK_DATASET = 'export_task_dataset', + EXPORT_JOB_DATASET = 'export_job_dataset', REMOVE_ANNO = 'remove_anno', OPEN_TASK = 'open_task', FINISH_JOB = 'finish_job', @@ -47,13 +42,10 @@ export enum Actions { function AnnotationMenuComponent(props: Props & RouteComponentProps): JSX.Element { const { - loaders, - loadActivity, jobInstance, stopFrame, history, onClickMenu, - onUploadAnnotations, removeAnnotations, setForceExitAnnotationFlag, saveAnnotations, @@ -192,30 +184,8 @@ function AnnotationMenuComponent(props: Props & RouteComponentProps): JSX.Elemen return ( onClickMenuWrapper(params)} className='cvat-annotation-menu' selectable={false}> - {LoadSubmenu({ - loaders, - loadActivity, - onFileUpload: (format: string, file: File): void => { - if (file) { - Modal.confirm({ - title: 'Current annotation will be lost', - content: 'You are going to upload new annotations to this job. Continue?', - className: 'cvat-modal-content-load-job-annotation', - onOk: () => { - onUploadAnnotations(format, file); - }, - okButtonProps: { - type: 'primary', - danger: true, - }, - okText: 'Update', - }); - } - }, - menuKey: Actions.LOAD_JOB_ANNO, - taskDimension: jobInstance.dimension, - })} - Export task dataset + Upload annotations + Export job dataset Remove annotations ; + sourceStorageLocation: StorageLocation; + targetStorageLocation: StorageLocation; + onChangeSourceStorageLocation?: (value: StorageLocation) => void; + onChangeTargetStorageLocation?: (value: StorageLocation) => void; +} + function NameConfigurationForm( { formRef, inputRef }: { formRef: RefObject, inputRef: RefObject }, @@ -99,9 +130,16 @@ function AdaptiveAutoAnnotationForm({ formRef }: { formRef: RefObject }): JSX.Element { +function AdvancedConfigurationForm(props: AdvancedConfigurationProps): JSX.Element { + const { + formRef, + sourceStorageLocation, + targetStorageLocation, + onChangeSourceStorageLocation, + onChangeTargetStorageLocation, + } = props; return ( -
    + + + + + + + + +
    ); } export default function CreateProjectContent(): JSX.Element { const [projectLabels, setProjectLabels] = useState([]); + const [sourceStorageLocation, setSourceStorageLocation] = useState(StorageLocation.LOCAL); + const [targetStorageLocation, setTargetStorageLocation] = useState(StorageLocation.LOCAL); const nameFormRef = useRef(null); const nameInputRef = useRef(null); const adaptiveAutoAnnotationFormRef = useRef(null); @@ -140,23 +198,32 @@ export default function CreateProjectContent(): JSX.Element { if (nameFormRef.current) nameFormRef.current.resetFields(); if (advancedFormRef.current) advancedFormRef.current.resetFields(); setProjectLabels([]); + setSourceStorageLocation(StorageLocation.LOCAL); + setTargetStorageLocation(StorageLocation.LOCAL); }; const focusForm = (): void => { nameInputRef.current?.focus(); }; - const sumbit = async (): Promise => { + const submit = async (): Promise => { try { let projectData: Record = {}; - if (nameFormRef.current && advancedFormRef.current) { + if (nameFormRef.current) { const basicValues = await nameFormRef.current.validateFields(); - const advancedValues = await advancedFormRef.current.validateFields(); + const advancedValues = advancedFormRef.current ? await advancedFormRef.current.validateFields() : {}; const adaptiveAutoAnnotationValues = await adaptiveAutoAnnotationFormRef.current?.validateFields(); + projectData = { ...projectData, ...advancedValues, name: basicValues.name, + source_storage: new Storage( + advancedValues.sourceStorage || { location: StorageLocation.LOCAL }, + ).toJSON(), + target_storage: new Storage( + advancedValues.targetStorage || { location: StorageLocation.LOCAL }, + ).toJSON(), }; if (adaptiveAutoAnnotationValues) { @@ -174,14 +241,14 @@ export default function CreateProjectContent(): JSX.Element { }; const onSubmitAndOpen = async (): Promise => { - const createdProject = await sumbit(); + const createdProject = await submit(); if (createdProject) { history.push(`/projects/${createdProject.id}`); } }; const onSubmitAndContinue = async (): Promise => { - const res = await sumbit(); + const res = await submit(); if (res) { resetForm(); notification.info({ @@ -216,7 +283,17 @@ export default function CreateProjectContent(): JSX.Element { /> - + + Advanced configuration}> + setSourceStorageLocation(value)} + onChangeTargetStorageLocation={(value: StorageLocation) => setTargetStorageLocation(value)} + /> + + diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx index 46818f59..4796af1c 100644 --- a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx @@ -1,12 +1,16 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React, { RefObject } from 'react'; import { Row, Col } from 'antd/lib/grid'; -import { PercentageOutlined } from '@ant-design/icons'; +import { PercentageOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import Input from 'antd/lib/input'; import Select from 'antd/lib/select'; +import Space from 'antd/lib/space'; +import Switch from 'antd/lib/switch'; +import Tooltip from 'antd/lib/tooltip'; import Radio from 'antd/lib/radio'; import Checkbox from 'antd/lib/checkbox'; import Form, { FormInstance, RuleObject, RuleRender } from 'antd/lib/form'; @@ -14,6 +18,13 @@ import Text from 'antd/lib/typography/Text'; import { Store } from 'antd/lib/form/interface'; import CVATTooltip from 'components/common/cvat-tooltip'; import patterns from 'utils/validation-patterns'; +import { StorageLocation } from 'reducers'; +import SourceStorageField from 'components/storage/source-storage-field'; +import TargetStorageField from 'components/storage/target-storage-field'; + +import { getCore, Storage, StorageData } from 'cvat-core-wrapper'; + +const core = getCore(); const { Option } = Select; @@ -40,6 +51,10 @@ export interface AdvancedConfiguration { useCache: boolean; copyData?: boolean; sortingMethod: SortingMethod; + useProjectSourceStorage: boolean; + useProjectTargetStorage: boolean; + sourceStorage: StorageData; + targetStorage: StorageData; } const initialValues: AdvancedConfiguration = { @@ -49,13 +64,33 @@ const initialValues: AdvancedConfiguration = { useCache: true, copyData: false, sortingMethod: SortingMethod.LEXICOGRAPHICAL, + useProjectSourceStorage: true, + useProjectTargetStorage: true, + + sourceStorage: { + location: StorageLocation.LOCAL, + cloudStorageId: undefined, + }, + targetStorage: { + location: StorageLocation.LOCAL, + cloudStorageId: undefined, + }, }; interface Props { onSubmit(values: AdvancedConfiguration): void; + onChangeUseProjectSourceStorage(value: boolean): void; + onChangeUseProjectTargetStorage(value: boolean): void; + onChangeSourceStorageLocation: (value: StorageLocation) => void; + onChangeTargetStorageLocation: (value: StorageLocation) => void; installedGit: boolean; + projectId: number | null; + useProjectSourceStorage: boolean; + useProjectTargetStorage: boolean; activeFileManagerTab: string; - dumpers: [] + dumpers: []; + sourceStorageLocation: StorageLocation; + targetStorageLocation: StorageLocation; } function validateURL(_: RuleObject, value: string): Promise { @@ -146,10 +181,15 @@ class AdvancedConfigurationForm extends React.PureComponent { } public submit(): Promise { - const { onSubmit } = this.props; + const { onSubmit, projectId } = this.props; + if (this.formRef.current) { - return this.formRef.current.validateFields().then( - (values: Store): Promise => { + if (projectId) { + return Promise.all([ + core.projects.get({ id: projectId }), + this.formRef.current.validateFields(), + ]).then(([getProjectResponse, values]) => { + const [project] = getProjectResponse; const frameFilter = values.frameStep ? `step=${values.frameStep}` : undefined; const entries = Object.entries(values).filter( (entry: [string, unknown]): boolean => entry[0] !== frameFilter, @@ -158,10 +198,33 @@ class AdvancedConfigurationForm extends React.PureComponent { onSubmit({ ...((Object.fromEntries(entries) as any) as AdvancedConfiguration), frameFilter, + sourceStorage: values.useProjectSourceStorage ? + new Storage(project.sourceStorage || { location: StorageLocation.LOCAL }) : + new Storage(values.sourceStorage), + targetStorage: values.useProjectTargetStorage ? + new Storage(project.targetStorage || { location: StorageLocation.LOCAL }) : + new Storage(values.targetStorage), }); return Promise.resolve(); - }, - ); + }); + } + return this.formRef.current.validateFields() + .then( + (values: Store): Promise => { + const frameFilter = values.frameStep ? `step=${values.frameStep}` : undefined; + const entries = Object.entries(values).filter( + (entry: [string, unknown]): boolean => entry[0] !== frameFilter, + ); + + onSubmit({ + ...((Object.fromEntries(entries) as any) as AdvancedConfiguration), + frameFilter, + sourceStorage: new Storage(values.sourceStorage), + targetStorage: new Storage(values.targetStorage), + }); + return Promise.resolve(); + }, + ); } return Promise.reject(new Error('Form ref is empty')); @@ -201,15 +264,15 @@ class AdvancedConfigurationForm extends React.PureComponent { ]} help='Specify how to sort images. It is not relevant for videos.' > - - + + Lexicographical - - Natural - + + Natural + Predefined - - Random + + Random ); @@ -291,15 +354,19 @@ class AdvancedConfigurationForm extends React.PureComponent { private renderGitLFSBox(): JSX.Element { return ( - - - Use LFS (Large File Support): - - + + + + + Use LFS (Large File Support): + + + + ); } @@ -374,25 +441,37 @@ class AdvancedConfigurationForm extends React.PureComponent { private renderUzeZipChunks(): JSX.Element { return ( - - - Use zip/video chunks - - + + + + + Use zip/video chunks + + + + ); } private renderCreateTaskMethod(): JSX.Element { return ( - - - Use cache - - + + + + + Use cache + + + + ); } @@ -423,6 +502,48 @@ class AdvancedConfigurationForm extends React.PureComponent { ); } + private renderSourceStorage(): JSX.Element { + const { + projectId, + useProjectSourceStorage, + sourceStorageLocation, + onChangeUseProjectSourceStorage, + onChangeSourceStorageLocation, + } = this.props; + return ( + + ); + } + + private renderTargetStorage(): JSX.Element { + const { + projectId, + useProjectTargetStorage, + targetStorageLocation, + onChangeUseProjectTargetStorage, + onChangeTargetStorageLocation, + } = this.props; + return ( + + ); + } + public render(): JSX.Element { const { installedGit, activeFileManagerTab } = this.props; return ( @@ -436,10 +557,8 @@ class AdvancedConfigurationForm extends React.PureComponent { ) : null} - {this.renderUzeZipChunks()} - - - {this.renderCreateTaskMethod()} + {this.renderUzeZipChunks()} + {this.renderCreateTaskMethod()} {this.renderImageQuality()} @@ -470,6 +589,14 @@ class AdvancedConfigurationForm extends React.PureComponent { {this.renderBugTracker()} + + + {this.renderSourceStorage()} + + + {this.renderTargetStorage()} + + ); } diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 49cc63f8..eefcc589 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -13,7 +14,8 @@ import notification from 'antd/lib/notification'; import Text from 'antd/lib/typography/Text'; // eslint-disable-next-line import/no-extraneous-dependencies import { ValidateErrorEntity } from 'rc-field-form/lib/interface'; - +import { StorageLocation } from 'reducers'; +import { getCore, Storage } from 'cvat-core-wrapper'; import ConnectedFileManager from 'containers/file-manager/file-manager'; import LabelsEditor from 'components/labels-editor/labels-editor'; import { Files } from 'components/file-manager/file-manager'; @@ -22,6 +24,8 @@ import ProjectSearchField from './project-search-field'; import ProjectSubsetField from './project-subset-field'; import AdvancedConfigurationForm, { AdvancedConfiguration, SortingMethod } from './advanced-configuration-form'; +const core = getCore(); + export interface CreateTaskData { projectId: number | null; basic: BaseConfiguration; @@ -55,6 +59,16 @@ const defaultState = { useZipChunks: true, useCache: true, sortingMethod: SortingMethod.LEXICOGRAPHICAL, + sourceStorage: { + location: StorageLocation.LOCAL, + cloudStorageId: undefined, + }, + targetStorage: { + location: StorageLocation.LOCAL, + cloudStorageId: undefined, + }, + useProjectSourceStorage: true, + useProjectTargetStorage: true, }, labels: [], files: { @@ -89,6 +103,17 @@ class CreateTaskContent extends React.PureComponent ({ + advanced: { + ...state.advanced, + [field]: { + location: value, + }, + }, + })); + } + private resetState = (): void => { this.basicConfigurationComponent.current?.resetFields(); this.advancedConfigurationComponent.current?.resetFields(); @@ -160,6 +185,24 @@ class CreateTaskContent extends React.PureComponent { + this.setState((state) => ({ + advanced: { + ...state.advanced, + useProjectSourceStorage: value, + }, + })); + }; + + private handleUseProjectTargetStorageChange = (value: boolean): void => { + this.setState((state) => ({ + advanced: { + ...state.advanced, + useProjectTargetStorage: value, + }, + })); + }; + private focusToForm = (): void => { this.basicConfigurationComponent.current?.focus(); }; @@ -189,6 +232,8 @@ class CreateTaskContent extends React.PureComponent => new Promise((resolve, reject) => { + const { projectId } = this.state; + if (!this.validateLabelsOrProject()) { notification.error({ message: 'Could not create a task', @@ -220,6 +265,26 @@ class CreateTaskContent extends React.PureComponent { + const [project] = response; + const { advanced } = this.state; + this.handleSubmitAdvancedConfiguration({ + ...advanced, + sourceStorage: new Storage( + project.sourceStorage || { location: StorageLocation.LOCAL }, + ), + targetStorage: new Storage( + project.targetStorage || { location: StorageLocation.LOCAL }, + ), + }); + return Promise.resolve(); + }) + .catch((error: Error): void => { + throw new Error(`Couldn't fetch the project ${projectId} ${error.toString()}`); + }); + } return Promise.resolve(); }) .then((): void => { @@ -341,7 +406,20 @@ class CreateTaskContent extends React.PureComponent @@ -352,6 +430,19 @@ class CreateTaskContent extends React.PureComponent { + this.handleChangeStorageLocation('sourceStorage', value); + }} + onChangeTargetStorageLocation={(value: StorageLocation) => { + this.handleChangeStorageLocation('targetStorage', value); + }} /> diff --git a/cvat-ui/src/components/create-task-page/styles.scss b/cvat-ui/src/components/create-task-page/styles.scss index 6cef28d3..c8c8896f 100644 --- a/cvat-ui/src/components/create-task-page/styles.scss +++ b/cvat-ui/src/components/create-task-page/styles.scss @@ -38,4 +38,8 @@ width: 100%; } } + + .cvat-settings-switch { + display: table-cell; + } } diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 114acd7d..af0a6d90 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -24,6 +25,9 @@ import GlobalErrorBoundary from 'components/global-error-boundary/global-error-b import ShortcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog'; import ExportDatasetModal from 'components/export-dataset/export-dataset-modal'; +import ExportBackupModal from 'components/export-backup/export-backup-modal'; +import ImportDatasetModal from 'components/import-dataset/import-dataset-modal'; +import ImportBackupModal from 'components/import-backup/import-backup-modal'; import ModelsPageContainer from 'containers/models-page/models-page'; import JobsPageComponent from 'components/jobs-page/jobs-page'; @@ -399,6 +403,9 @@ class CVATApplication extends React.PureComponent {/* eslint-disable-next-line */} + + + {/* eslint-disable-next-line */}
    diff --git a/cvat-ui/src/components/export-backup/export-backup-modal.tsx b/cvat-ui/src/components/export-backup/export-backup-modal.tsx new file mode 100644 index 00000000..838070fd --- /dev/null +++ b/cvat-ui/src/components/export-backup/export-backup-modal.tsx @@ -0,0 +1,150 @@ +// Copyright (c) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useState, useEffect, useCallback } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import Modal from 'antd/lib/modal'; +import Notification from 'antd/lib/notification'; +import Text from 'antd/lib/typography/Text'; +import Input from 'antd/lib/input'; +import Form from 'antd/lib/form'; +import { CombinedState, StorageLocation } from 'reducers'; +import { exportActions, exportBackupAsync } from 'actions/export-actions'; +import { getCore, Storage, StorageData } from 'cvat-core-wrapper'; + +import TargetStorageField from 'components/storage/target-storage-field'; + +const core = getCore(); + +type FormValues = { + customName: string | undefined; + targetStorage: StorageData; + useProjectTargetStorage: boolean; +}; + +const initialValues: FormValues = { + customName: undefined, + targetStorage: { + location: StorageLocation.LOCAL, + cloudStorageId: undefined, + }, + useProjectTargetStorage: true, +}; + +function ExportBackupModal(): JSX.Element { + const dispatch = useDispatch(); + const [form] = Form.useForm(); + const [instanceType, setInstanceType] = useState(''); + const [useDefaultStorage, setUseDefaultStorage] = useState(true); + const [storageLocation, setStorageLocation] = useState(StorageLocation.LOCAL); + const [defaultStorageLocation, setDefaultStorageLocation] = useState(StorageLocation.LOCAL); + const [defaultStorageCloudId, setDefaultStorageCloudId] = useState(null); + const [helpMessage, setHelpMessage] = useState(''); + + const instanceT = useSelector((state: CombinedState) => state.export.instanceType); + const instance = useSelector((state: CombinedState) => { + if (!instanceT) { + return null; + } + return state.export[`${instanceT}s` as 'projects' | 'tasks']?.backup?.modalInstance; + }); + + useEffect(() => { + if (instance instanceof core.classes.Project) { + setInstanceType(`project #${instance.id}`); + } else if (instance instanceof core.classes.Task) { + setInstanceType(`task #${instance.id}`); + } + }, [instance]); + + useEffect(() => { + if (instance) { + setDefaultStorageLocation(instance.targetStorage?.location || StorageLocation.LOCAL); + setDefaultStorageCloudId(instance.targetStorage?.cloudStorageId || null); + } + }, [instance]); + + useEffect(() => { + // eslint-disable-next-line prefer-template + const message = `Export backup to ${(defaultStorageLocation) ? defaultStorageLocation.split('_')[0] : 'local'} ` + + `storage ${(defaultStorageCloudId) ? `â„–${defaultStorageCloudId}` : ''}`; + setHelpMessage(message); + }, [defaultStorageLocation, defaultStorageCloudId]); + + const closeModal = (): void => { + setUseDefaultStorage(true); + setStorageLocation(StorageLocation.LOCAL); + form.resetFields(); + dispatch(exportActions.closeExportDatasetModal(instance)); + }; + + const handleExport = useCallback( + (values: FormValues): void => { + dispatch( + exportBackupAsync( + instance, + new Storage({ + location: useDefaultStorage ? defaultStorageLocation : values.targetStorage?.location, + cloudStorageId: useDefaultStorage ? ( + defaultStorageCloudId + ) : ( + values.targetStorage?.cloudStorageId + ), + }), + useDefaultStorage, + values.customName ? `${values.customName}.zip` : undefined, + ), + ); + closeModal(); + Notification.info({ + message: 'Backup export started', + description: + 'Backup export was started. ' + + 'Download will start automatically as soon as the file is ready.', + className: 'cvat-notification-notice-export-backup-start', + }); + }, + [instance, useDefaultStorage, defaultStorageLocation, defaultStorageCloudId], + ); + + return ( + {`Export ${instanceType}`}} + visible={!!instance} + onCancel={closeModal} + onOk={() => form.submit()} + className={`cvat-modal-export-${instanceType.split(' ')[0]}`} + destroyOnClose + > +
    + Custom name} name='customName'> + + + setUseDefaultStorage(value)} + onChangeLocationValue={(value: StorageLocation) => setStorageLocation(value)} + /> + +
    + ); +} + +export default React.memo(ExportBackupModal); diff --git a/cvat-ui/src/components/export-backup/styles.scss b/cvat-ui/src/components/export-backup/styles.scss new file mode 100644 index 00000000..92c21f8e --- /dev/null +++ b/cvat-ui/src/components/export-backup/styles.scss @@ -0,0 +1,13 @@ +// Copyright (c) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-modal-export-option-item > .ant-select-item-option-content, +.cvat-modal-export-select .ant-select-selection-item { + > span[role='img'] { + color: $info-icon-color; + margin-right: $grid-unit-size; + } +} diff --git a/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx b/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx index 315c394c..675796fd 100644 --- a/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx +++ b/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx @@ -1,22 +1,24 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import './styles.scss'; import React, { useState, useEffect, useCallback } from 'react'; +import { connect, useDispatch } from 'react-redux'; import Modal from 'antd/lib/modal'; import Notification from 'antd/lib/notification'; -import { useSelector, useDispatch } from 'react-redux'; import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; import Text from 'antd/lib/typography/Text'; import Select from 'antd/lib/select'; -import Checkbox from 'antd/lib/checkbox'; import Input from 'antd/lib/input'; import Form from 'antd/lib/form'; - -import { CombinedState } from 'reducers'; +import Switch from 'antd/lib/switch'; +import Space from 'antd/lib/space'; +import TargetStorageField from 'components/storage/target-storage-field'; +import { CombinedState, StorageLocation } from 'reducers'; import { exportActions, exportDatasetAsync } from 'actions/export-actions'; -import { getCore } from 'cvat-core-wrapper'; +import { getCore, Storage, StorageData } from 'cvat-core-wrapper'; const core = getCore(); @@ -24,43 +26,94 @@ type FormValues = { selectedFormat: string | undefined; saveImages: boolean; customName: string | undefined; + targetStorage: StorageData; + useProjectTargetStorage: boolean; +}; + +const initialValues: FormValues = { + selectedFormat: undefined, + saveImages: false, + customName: undefined, + targetStorage: { + location: StorageLocation.LOCAL, + cloudStorageId: undefined, + }, + useProjectTargetStorage: true, }; -function ExportDatasetModal(): JSX.Element { +function ExportDatasetModal(props: StateToProps): JSX.Element { + const { + dumpers, + instance, + current, + } = props; + const [instanceType, setInstanceType] = useState(''); - const [activities, setActivities] = useState([]); + + const [useDefaultTargetStorage, setUseDefaultTargetStorage] = useState(true); const [form] = Form.useForm(); + const [targetStorage, setTargetStorage] = useState({ + location: StorageLocation.LOCAL, + }); + const [defaultStorageLocation, setDefaultStorageLocation] = useState(StorageLocation.LOCAL); + const [defaultStorageCloudId, setDefaultStorageCloudId] = useState(null); + const [helpMessage, setHelpMessage] = useState(''); const dispatch = useDispatch(); - const instance = useSelector((state: CombinedState) => state.export.instance); - const modalVisible = useSelector((state: CombinedState) => state.export.modalVisible); - const dumpers = useSelector((state: CombinedState) => state.formats.annotationFormats.dumpers); - const { tasks: taskExportActivities, projects: projectExportActivities } = useSelector( - (state: CombinedState) => state.export, - ); - const initActivities = (): void => { + useEffect(() => { if (instance instanceof core.classes.Project) { setInstanceType(`project #${instance.id}`); - setActivities(projectExportActivities[instance.id] || []); - } else if (instance) { - const taskID = instance instanceof core.classes.Task ? instance.id : instance.taskId; - setInstanceType(`task #${taskID}`); - setActivities(taskExportActivities[taskID] || []); + } else if (instance instanceof core.classes.Task || instance instanceof core.classes.Job) { + if (instance instanceof core.classes.Task) { + setInstanceType(`task #${instance.id}`); + } else { + setInstanceType(`job #${instance.id}`); + } if (instance.mode === 'interpolation' && instance.dimension === '2d') { form.setFieldsValue({ selectedFormat: 'CVAT for video 1.1' }); } else if (instance.mode === 'annotation' && instance.dimension === '2d') { form.setFieldsValue({ selectedFormat: 'CVAT for images 1.1' }); } } - }; + }, [instance]); useEffect(() => { - initActivities(); - }, [instance?.id, instance instanceof core.classes.Project, taskExportActivities, projectExportActivities]); + if (instance) { + if (instance instanceof core.classes.Project || instance instanceof core.classes.Task) { + setDefaultStorageLocation(instance.targetStorage?.location || StorageLocation.LOCAL); + setDefaultStorageCloudId(instance.targetStorage?.cloudStorageId || null); + } else { + core.tasks.get({ id: instance.taskId }) + .then((response: any) => { + if (response.length) { + const [taskInstance] = response; + setDefaultStorageLocation(taskInstance.targetStorage?.location || StorageLocation.LOCAL); + setDefaultStorageCloudId(taskInstance.targetStorage?.cloudStorageId || null); + } + }) + .catch((error: Error) => { + if ((error as any).code !== 403) { + Notification.error({ + message: `Could not fetch the task ${instance.taskId}`, + description: error.toString(), + }); + } + }); + } + } + }, [instance]); + + useEffect(() => { + // eslint-disable-next-line prefer-template + setHelpMessage(`Export to ${(defaultStorageLocation) ? defaultStorageLocation.split('_')[0] : 'local'} ` + + `storage ${(defaultStorageCloudId) ? `â„–${defaultStorageCloudId}` : ''}`); + }, [defaultStorageLocation, defaultStorageCloudId]); const closeModal = (): void => { + setUseDefaultTargetStorage(true); + setTargetStorage({ location: StorageLocation.LOCAL }); form.resetFields(); - dispatch(exportActions.closeExportModal()); + dispatch(exportActions.closeExportDatasetModal(instance)); }; const handleExport = useCallback( @@ -70,26 +123,32 @@ function ExportDatasetModal(): JSX.Element { exportDatasetAsync( instance, values.selectedFormat as string, - values.customName ? `${values.customName}.zip` : '', values.saveImages, + useDefaultTargetStorage, + useDefaultTargetStorage ? new Storage({ + location: defaultStorageLocation, + cloudStorageId: defaultStorageCloudId, + }) : new Storage(targetStorage), + values.customName ? `${values.customName}.zip` : null, ), ); closeModal(); + const resource = values.saveImages ? 'Dataset' : 'Annotations'; Notification.info({ - message: 'Dataset export started', + message: `${resource} export started`, description: - `Dataset export was started for ${instanceType}. ` + - 'Download will start automatically as soon as the dataset is ready.', + `${resource} export was started for ${instanceType}. ` + + `Download will start automatically as soon as the ${resource} is ready.`, className: `cvat-notification-notice-export-${instanceType.split(' ')[0]}-start`, }); }, - [instance, instanceType], + [instance, instanceType, useDefaultTargetStorage, defaultStorageLocation, defaultStorageCloudId, targetStorage], ); return ( {`Export ${instanceType} as a dataset`}} + visible={!!instance} onCancel={closeModal} onOk={() => form.submit()} className={`cvat-modal-export-${instanceType.split(' ')[0]}`} @@ -98,20 +157,13 @@ function ExportDatasetModal(): JSX.Element {
    Export format} rules={[{ required: true, message: 'Format must be selected' }]} > + setUseDefaultTargetStorage(value)} + onChangeStorage={(value: StorageData) => setTargetStorage(value)} + onChangeLocationValue={(value: StorageLocation) => { + setTargetStorage({ location: value }); + }} + />
    ); } -export default React.memo(ExportDatasetModal); +interface StateToProps { + dumpers: any; + instance: any; + current: any; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { instanceType } = state.export; + const instance = !instanceType ? null : ( + state.export[`${instanceType}s` as 'projects' | 'tasks' | 'jobs'] + ).dataset.modalInstance; + + return { + instance, + current: !instanceType ? [] : ( + state.export[`${instanceType}s` as 'projects' | 'tasks' | 'jobs'] + ).dataset.current[instance.id], + dumpers: state.formats.annotationFormats.dumpers, + }; +} + +export default connect(mapStateToProps)(ExportDatasetModal); diff --git a/cvat-ui/src/components/export-dataset/styles.scss b/cvat-ui/src/components/export-dataset/styles.scss index 40dba347..ca37813e 100644 --- a/cvat-ui/src/components/export-dataset/styles.scss +++ b/cvat-ui/src/components/export-dataset/styles.scss @@ -11,3 +11,7 @@ margin-right: $grid-unit-size; } } + +.cvat-modal-export-switch-use-default-storage { + display: table-cell; +} diff --git a/cvat-ui/src/components/file-manager/cloud-storages-tab.tsx b/cvat-ui/src/components/file-manager/cloud-storages-tab.tsx index f292c4ba..d78c4399 100644 --- a/cvat-ui/src/components/file-manager/cloud-storages-tab.tsx +++ b/cvat-ui/src/components/file-manager/cloud-storages-tab.tsx @@ -1,20 +1,14 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import './styles.scss'; import React, { useEffect, useState } from 'react'; import Form from 'antd/lib/form'; -import notification from 'antd/lib/notification'; -import AutoComplete from 'antd/lib/auto-complete'; -import Input from 'antd/lib/input'; -import { debounce } from 'lodash'; - import Select from 'antd/lib/select'; -import { getCore } from 'cvat-core-wrapper'; import { CloudStorage } from 'reducers'; -import { AzureProvider, GoogleCloudProvider, S3Provider } from 'icons'; -import { ProviderType } from 'utils/enums'; +import SelectCloudStorage from 'components/select-cloud-storage/select-cloud-storage'; import CloudStorageFiles from './cloud-storages-files'; interface Props { @@ -27,61 +21,15 @@ interface Props { onSelectCloudStorage: (cloudStorageId: number | null) => void; } -async function searchCloudStorages(filter: Record): Promise { - try { - const data = await getCore().cloudStorages.get(filter); - return data; - } catch (error) { - notification.error({ - message: 'Could not fetch a list of cloud storages', - description: error.toString(), - }); - } - - return []; -} - const { Option } = Select; -const searchCloudStoragesWrapper = debounce((phrase, setList) => { - const filter = { - filter: JSON.stringify({ - and: [{ - '==': [{ var: 'display_name' }, phrase], - }], - }), - }; - searchCloudStorages(filter).then((list) => { - setList(list); - }); -}, 500); - export default function CloudStorageTab(props: Props): JSX.Element { const { searchPhrase, setSearchPhrase } = props; - const [initialList, setInitialList] = useState([]); - const [list, setList] = useState([]); const { formRef, cloudStorage, selectedFiles, onSelectFiles, onSelectCloudStorage, } = props; const [selectedManifest, setSelectedManifest] = useState(null); - useEffect(() => { - searchCloudStorages({}).then((data) => { - setInitialList(data); - if (!list.length) { - setList(data); - } - }); - }, []); - - useEffect(() => { - if (!searchPhrase) { - setList(initialList); - } else { - searchCloudStoragesWrapper(searchPhrase, setList); - } - }, [searchPhrase, initialList]); - useEffect(() => { if (cloudStorage) { setSelectedManifest(cloudStorage.manifests[0]); @@ -94,67 +42,15 @@ export default function CloudStorageTab(props: Props): JSX.Element { } }, [selectedManifest]); - const onBlur = (): void => { - if (!searchPhrase && cloudStorage) { - onSelectCloudStorage(null); - } else if (searchPhrase) { - const potentialStorages = list.filter((_cloudStorage) => _cloudStorage.displayName.includes(searchPhrase)); - if (potentialStorages.length === 1) { - const potentialStorage = potentialStorages[0]; - setSearchPhrase(potentialStorage.displayName); - // eslint-disable-next-line prefer-destructuring - potentialStorage.manifestPath = potentialStorage.manifests[0]; - onSelectCloudStorage(potentialStorage); - } - } - }; - return (
    - - { - setSearchPhrase(phrase); - }} - options={list.map((_cloudStorage) => ({ - value: _cloudStorage.id.toString(), - label: ( - - {_cloudStorage.providerType === ProviderType.AWS_S3_BUCKET && } - {_cloudStorage.providerType === ProviderType.AZURE_CONTAINER && } - { - _cloudStorage.providerType === ProviderType.GOOGLE_CLOUD_STORAGE && - - } - {_cloudStorage.displayName} - - ), - }))} - onSelect={(value: string) => { - const selectedCloudStorage = - list.filter((_cloudStorage: CloudStorage) => _cloudStorage.id === +value)[0] || null; - // eslint-disable-next-line prefer-destructuring - selectedCloudStorage.manifestPath = selectedCloudStorage.manifests[0]; - onSelectCloudStorage(selectedCloudStorage); - setSearchPhrase(selectedCloudStorage?.displayName || ''); - }} - allowClear - > - - - + {cloudStorage ? ( (null); + const instanceType = useSelector((state: CombinedState) => state.import.instanceType); + const modalVisible = useSelector((state: CombinedState) => { + if (instanceType && ['project', 'task'].includes(instanceType)) { + return state.import[`${instanceType}s` as 'projects' | 'tasks'].backup.modalVisible; + } + return false; + }); + const dispatch = useDispatch(); + const [selectedSourceStorage, setSelectedSourceStorage] = useState({ + location: StorageLocation.LOCAL, + }); + + const uploadLocalFile = (): JSX.Element => ( + { + if (!['application/zip', 'application/x-zip-compressed'].includes(_file.type)) { + message.error('Only ZIP archive is supported'); + } else { + setFile(_file); + } + return false; + }} + onRemove={() => { + setFile(null); + }} + > +

    + +

    +

    Click or drag file to this area

    +
    + ); + + const validateFileName = (_: RuleObject, value: string): Promise => { + if (value) { + const extension = value.toLowerCase().split('.')[1]; + if (extension !== 'zip') { + return Promise.reject(new Error('Only ZIP archive is supported')); + } + } + + return Promise.resolve(); + }; + + const renderCustomName = (): JSX.Element => ( + File name} + name='fileName' + rules={[{ validator: validateFileName }]} + > + + + ); + + const closeModal = useCallback((): void => { + setSelectedSourceStorage({ + location: StorageLocation.LOCAL, + }); + setFile(null); + dispatch(importActions.closeImportBackupModal(instanceType as 'project' | 'task')); + form.resetFields(); + }, [form, instanceType]); + + const handleImport = useCallback( + (values: FormValues): void => { + if (file === null && !values.fileName) { + Notification.error({ + message: 'No backup file specified', + }); + return; + } + const sourceStorage = new Storage({ + location: values.sourceStorage.location, + cloudStorageId: values.sourceStorage?.cloudStorageId, + }); + + dispatch(importBackupAsync(instanceType, sourceStorage, file || (values.fileName) as string)); + + Notification.info({ + message: `The ${instanceType} creating from the backup has been started`, + className: 'cvat-notification-notice-import-backup-start', + }); + closeModal(); + }, + [instanceType, file], + ); + + return ( + <> + + Create + {instanceType} + {' '} + from backup + + )} + visible={modalVisible} + onCancel={closeModal} + onOk={() => form.submit()} + className='cvat-modal-import-backup' + > + + setSelectedSourceStorage(new Storage(value))} + onChangeLocationValue={(value: StorageLocation) => { + setSelectedSourceStorage({ + location: value, + }); + }} + + /> + {selectedSourceStorage?.location === StorageLocation.CLOUD_STORAGE && renderCustomName()} + {selectedSourceStorage?.location === StorageLocation.LOCAL && uploadLocalFile()} + + + + ); +} + +export default React.memo(ImportBackupModal); diff --git a/cvat-ui/src/components/import-dataset-modal/import-dataset-modal.tsx b/cvat-ui/src/components/import-dataset-modal/import-dataset-modal.tsx deleted file mode 100644 index f395390b..00000000 --- a/cvat-ui/src/components/import-dataset-modal/import-dataset-modal.tsx +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (C) 2021-2022 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import './styles.scss'; -import React, { useCallback, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Modal from 'antd/lib/modal'; -import Form from 'antd/lib/form'; -import Text from 'antd/lib/typography/Text'; -import Select from 'antd/lib/select'; -import Notification from 'antd/lib/notification'; -import message from 'antd/lib/message'; -import Upload, { RcFile } from 'antd/lib/upload'; - -import { - UploadOutlined, InboxOutlined, LoadingOutlined, QuestionCircleOutlined, -} from '@ant-design/icons'; - -import CVATTooltip from 'components/common/cvat-tooltip'; -import { CombinedState } from 'reducers'; -import { importActions, importDatasetAsync } from 'actions/import-actions'; - -import ImportDatasetStatusModal from './import-dataset-status-modal'; - -type FormValues = { - selectedFormat: string | undefined; -}; - -function ImportDatasetModal(): JSX.Element { - const [form] = Form.useForm(); - const [file, setFile] = useState(null); - const modalVisible = useSelector((state: CombinedState) => state.import.modalVisible); - const instance = useSelector((state: CombinedState) => state.import.instance); - const currentImportId = useSelector((state: CombinedState) => state.import.importingId); - const importers = useSelector((state: CombinedState) => state.formats.annotationFormats.loaders); - const dispatch = useDispatch(); - - const closeModal = useCallback((): void => { - form.resetFields(); - setFile(null); - dispatch(importActions.closeImportModal()); - }, [form]); - - const handleImport = useCallback( - (values: FormValues): void => { - if (file === null) { - Notification.error({ - message: 'No dataset file selected', - }); - return; - } - dispatch(importDatasetAsync(instance, values.selectedFormat as string, file)); - closeModal(); - Notification.info({ - message: 'Dataset import started', - description: `Dataset import was started for project #${instance?.id}. `, - className: 'cvat-notification-notice-import-dataset-start', - }); - }, - [instance?.id, file], - ); - - return ( - <> - - Import dataset to project - - - - - )} - visible={modalVisible} - onCancel={closeModal} - onOk={() => form.submit()} - className='cvat-modal-import-dataset' - > -
    - - - - { - if (!['application/zip', 'application/x-zip-compressed'].includes(_file.type)) { - message.error('Only ZIP archive is supported'); - } else { - setFile(_file); - } - return false; - }} - onRemove={() => { - setFile(null); - }} - > -

    - -

    -

    Click or drag file to this area

    -
    -
    -
    - - - ); -} - -export default React.memo(ImportDatasetModal); diff --git a/cvat-ui/src/components/import-dataset-modal/import-dataset-status-modal.tsx b/cvat-ui/src/components/import-dataset-modal/import-dataset-status-modal.tsx deleted file mode 100644 index 6c4645d2..00000000 --- a/cvat-ui/src/components/import-dataset-modal/import-dataset-status-modal.tsx +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2021-2022 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import './styles.scss'; -import React from 'react'; -import { useSelector } from 'react-redux'; -import Modal from 'antd/lib/modal'; -import Alert from 'antd/lib/alert'; -import Progress from 'antd/lib/progress'; - -import { CombinedState } from 'reducers'; - -function ImportDatasetStatusModal(): JSX.Element { - const currentImportId = useSelector((state: CombinedState) => state.import.importingId); - const progress = useSelector((state: CombinedState) => state.import.progress); - const status = useSelector((state: CombinedState) => state.import.status); - - return ( - - - - - ); -} - -export default React.memo(ImportDatasetStatusModal); diff --git a/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx b/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx new file mode 100644 index 00000000..fc6c426e --- /dev/null +++ b/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx @@ -0,0 +1,463 @@ +// Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useCallback, useEffect, useState } from 'react'; +import { connect, useDispatch } from 'react-redux'; +import Modal from 'antd/lib/modal'; +import Form, { RuleObject } from 'antd/lib/form'; +import Text from 'antd/lib/typography/Text'; +import Select from 'antd/lib/select'; +import Notification from 'antd/lib/notification'; +import message from 'antd/lib/message'; +import Upload, { RcFile } from 'antd/lib/upload'; +import Input from 'antd/lib/input/Input'; +import { + UploadOutlined, InboxOutlined, LoadingOutlined, QuestionCircleOutlined, +} from '@ant-design/icons'; +import CVATTooltip from 'components/common/cvat-tooltip'; +import { CombinedState, StorageLocation } from 'reducers'; +import { importActions, importDatasetAsync } from 'actions/import-actions'; +import Space from 'antd/lib/space'; +import Switch from 'antd/lib/switch'; +import { getCore, Storage, StorageData } from 'cvat-core-wrapper'; +import StorageField from 'components/storage/storage-field'; +import ImportDatasetStatusModal from './import-dataset-status-modal'; + +const { confirm } = Modal; + +const core = getCore(); + +type FormValues = { + selectedFormat: string | undefined; + fileName?: string | undefined; + sourceStorage: StorageData; + useDefaultSettings: boolean; +}; + +const initialValues: FormValues = { + selectedFormat: undefined, + fileName: undefined, + sourceStorage: { + location: StorageLocation.LOCAL, + cloudStorageId: undefined, + }, + useDefaultSettings: true, +}; + +interface UploadParams { + resource: 'annotation' | 'dataset'; + useDefaultSettings: boolean; + sourceStorage: Storage; + selectedFormat: string | null; + file: File | null; + fileName: string | null; +} + +function ImportDatasetModal(props: StateToProps): JSX.Element { + const { + importers, + instanceT, + instance, + current, + } = props; + const [form] = Form.useForm(); + const dispatch = useDispatch(); + // TODO useState -> useReducer + const [instanceType, setInstanceType] = useState(''); + const [file, setFile] = useState(null); + const [selectedLoader, setSelectedLoader] = useState(null); + const [useDefaultSettings, setUseDefaultSettings] = useState(true); + const [defaultStorageLocation, setDefaultStorageLocation] = useState(StorageLocation.LOCAL); + const [defaultStorageCloudId, setDefaultStorageCloudId] = useState(undefined); + const [helpMessage, setHelpMessage] = useState(''); + const [selectedSourceStorageLocation, setSelectedSourceStorageLocation] = useState(StorageLocation.LOCAL); + const [uploadParams, setUploadParams] = useState({ + useDefaultSettings: true, + } as UploadParams); + const [resource, setResource] = useState(''); + + useEffect(() => { + if (instanceT === 'project') { + setResource('dataset'); + } else if (instanceT === 'task' || instanceT === 'job') { + setResource('annotation'); + } + }, [instanceT]); + + const isDataset = useCallback((): boolean => resource === 'dataset', [resource]); + + const isAnnotation = useCallback((): boolean => resource === 'annotation', [resource]); + + useEffect(() => { + setUploadParams({ + ...uploadParams, + resource, + sourceStorage: { + location: defaultStorageLocation, + cloudStorageId: defaultStorageCloudId, + } as Storage, + } as UploadParams); + }, [resource, defaultStorageLocation, defaultStorageCloudId]); + + useEffect(() => { + if (instance) { + if (instance instanceof core.classes.Project || instance instanceof core.classes.Task) { + setDefaultStorageLocation(instance.sourceStorage?.location || StorageLocation.LOCAL); + setDefaultStorageCloudId(instance.sourceStorage?.cloudStorageId || null); + if (instance instanceof core.classes.Project) { + setInstanceType(`project #${instance.id}`); + } else { + setInstanceType(`task #${instance.id}`); + } + } else if (instance instanceof core.classes.Job) { + core.tasks.get({ id: instance.taskId }) + .then((response: any) => { + if (response.length) { + const [taskInstance] = response; + setDefaultStorageLocation(taskInstance.sourceStorage?.location || StorageLocation.LOCAL); + setDefaultStorageCloudId(taskInstance.sourceStorage?.cloudStorageId || null); + } + }) + .catch((error: Error) => { + if ((error as any).code !== 403) { + Notification.error({ + message: `Could not get task instance ${instance.taskId}`, + description: error.toString(), + }); + } + }); + setInstanceType(`job #${instance.id}`); + } + } + }, [instance, resource]); + + useEffect(() => { + setHelpMessage( + // eslint-disable-next-line prefer-template + `Import from ${(defaultStorageLocation) ? defaultStorageLocation.split('_')[0] : 'local'} ` + + `storage ${(defaultStorageCloudId) ? `â„–${defaultStorageCloudId}` : ''}`, + ); + }, [defaultStorageLocation, defaultStorageCloudId]); + + const uploadLocalFile = (): JSX.Element => ( + { + if (!selectedLoader) { + message.warn('Please select a format first', 3); + } else if (isDataset() && !['application/zip', 'application/x-zip-compressed'].includes(_file.type)) { + message.error('Only ZIP archive is supported for import a dataset'); + } else if (isAnnotation() && + !selectedLoader.format.toLowerCase().split(', ').includes(_file.name.split('.')[_file.name.split('.').length - 1])) { + message.error( + `For ${selectedLoader.name} format only files with ` + + `${selectedLoader.format.toLowerCase()} extension can be used`, + ); + } else { + setFile(_file); + setUploadParams({ + ...uploadParams, + file: _file, + } as UploadParams); + } + return false; + }} + onRemove={() => { + setFile(null); + }} + > +

    + +

    +

    Click or drag file to this area

    +
    + ); + + const validateFileName = (_: RuleObject, value: string): Promise => { + if (!selectedLoader) { + message.warn('Please select a format first', 3); + return Promise.reject(); + } + if (value) { + const extension = value.toLowerCase().split('.')[value.split('.').length - 1]; + if (isAnnotation()) { + const allowedExtensions = selectedLoader.format.toLowerCase().split(', '); + if (!allowedExtensions.includes(extension)) { + return Promise.reject(new Error( + `For ${selectedLoader.name} format only files with ` + + `${selectedLoader.format.toLowerCase()} extension can be used`, + )); + } + } + if (isDataset()) { + if (extension !== 'zip') { + return Promise.reject(new Error('Only ZIP archive is supported for import a dataset')); + } + } + } + + return Promise.resolve(); + }; + + const renderCustomName = (): JSX.Element => ( + File name} + name='fileName' + hasFeedback + dependencies={['selectedFormat']} + rules={[{ validator: validateFileName }]} + required + > + ) => { + if (e.target.value) { + setUploadParams({ + ...uploadParams, + fileName: e.target.value, + } as UploadParams); + } + }} + /> + + ); + + const closeModal = useCallback((): void => { + setUseDefaultSettings(true); + setSelectedSourceStorageLocation(StorageLocation.LOCAL); + form.resetFields(); + setFile(null); + dispatch(importActions.closeImportDatasetModal(instance)); + }, [form, instance]); + + const onUpload = (): void => { + if (uploadParams && uploadParams.resource) { + dispatch(importDatasetAsync( + instance, uploadParams.selectedFormat as string, + uploadParams.useDefaultSettings, uploadParams.sourceStorage, + uploadParams.file || uploadParams.fileName as string, + )); + const resToPrint = uploadParams.resource.charAt(0).toUpperCase() + uploadParams.resource.slice(1); + Notification.info({ + message: `${resToPrint} import started`, + description: `${resToPrint} import was started for ${instanceType}. `, + className: `cvat-notification-notice-import-${uploadParams.resource}-start`, + }); + } + }; + + const confirmUpload = (): void => { + confirm({ + title: 'Current annotation will be lost', + content: `You are going to upload new annotations to ${instanceType}. Continue?`, + className: `cvat-modal-content-load-${instanceType.split(' ')[0]}-annotation`, + onOk: () => { + onUpload(); + }, + okButtonProps: { + type: 'primary', + danger: true, + }, + okText: 'Update', + }); + }; + + const handleImport = useCallback( + (values: FormValues): void => { + if (uploadParams.file === null && !values.fileName) { + Notification.error({ + message: `No ${uploadParams.resource} file specified`, + }); + return; + } + + if (isAnnotation()) { + confirmUpload(); + } else { + onUpload(); + } + closeModal(); + }, + [instance, uploadParams], + ); + + return ( + <> + + + {`Import ${resource} to ${instanceType}`} + + { + instance instanceof core.classes.Project && ( + + + + ) + } + + )} + visible={!!instance} + onCancel={closeModal} + onOk={() => form.submit()} + className='cvat-modal-import-dataset' + > +
    + + + + + + { + setUseDefaultSettings(value); + setUploadParams({ + ...uploadParams, + useDefaultSettings: value, + } as UploadParams); + }} + /> + + Use default settings + + + + + + { + useDefaultSettings && ( + defaultStorageLocation === StorageLocation.LOCAL || + defaultStorageLocation === null + ) && uploadLocalFile() + } + { + useDefaultSettings && + defaultStorageLocation === StorageLocation.CLOUD_STORAGE && + renderCustomName() + } + {!useDefaultSettings && ( + { + setUploadParams({ + ...uploadParams, + sourceStorage: new Storage({ + location: value?.location || defaultStorageLocation, + cloudStorageId: (value.location) ? value.cloudStorageId : defaultStorageCloudId, + }), + } as UploadParams); + }} + locationValue={selectedSourceStorageLocation} + onChangeLocationValue={(value: StorageLocation) => setSelectedSourceStorageLocation(value)} + /> + )} + { + !useDefaultSettings && + selectedSourceStorageLocation === StorageLocation.CLOUD_STORAGE && + renderCustomName() + } + { + !useDefaultSettings && + selectedSourceStorageLocation === StorageLocation.LOCAL && + uploadLocalFile() + } + +
    + + + ); +} + +interface StateToProps { + importers: any; + instanceT: 'project' | 'task' | 'job' | null; + instance: any; + current: any; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { instanceType } = state.import; + + return { + importers: state.formats.annotationFormats.loaders, + instanceT: instanceType, + instance: !instanceType ? null : ( + state.import[`${instanceType}s` as 'projects' | 'tasks' | 'jobs'] + ).dataset.modalInstance, + current: !instanceType ? null : ( + state.import[`${instanceType}s` as 'projects' | 'tasks' | 'jobs'] + ).dataset.current, + }; +} + +export default connect(mapStateToProps)(ImportDatasetModal); diff --git a/cvat-ui/src/components/import-dataset/import-dataset-status-modal.tsx b/cvat-ui/src/components/import-dataset/import-dataset-status-modal.tsx new file mode 100644 index 00000000..593b532e --- /dev/null +++ b/cvat-ui/src/components/import-dataset/import-dataset-status-modal.tsx @@ -0,0 +1,58 @@ +// Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import Modal from 'antd/lib/modal'; +import Alert from 'antd/lib/alert'; +import Progress from 'antd/lib/progress'; + +import { CombinedState } from 'reducers'; + +function ImportDatasetStatusModal(): JSX.Element { + const current = useSelector((state: CombinedState) => state.import.projects.dataset.current); + const [importingId, setImportingId] = useState(null); + + useEffect(() => { + const [id] = Object.keys(current); + setImportingId(parseInt(id, 10)); + }, [current]); + + const importing = useSelector((state: CombinedState) => { + if (!importingId) { + return false; + } + return !!state.import.projects.dataset.current[importingId]; + }); + const progress = useSelector((state: CombinedState) => { + if (!importingId) { + return 0; + } + return state.import.projects.dataset.current[importingId]?.progress; + }); + const status = useSelector((state: CombinedState) => { + if (!importingId) { + return ''; + } + return state.import.projects.dataset.current[importingId]?.status; + }); + + return ( + + + + + ); +} + +export default React.memo(ImportDatasetStatusModal); diff --git a/cvat-ui/src/components/import-dataset-modal/styles.scss b/cvat-ui/src/components/import-dataset/styles.scss similarity index 90% rename from cvat-ui/src/components/import-dataset-modal/styles.scss rename to cvat-ui/src/components/import-dataset/styles.scss index ef72e86c..14c9c3c0 100644 --- a/cvat-ui/src/components/import-dataset-modal/styles.scss +++ b/cvat-ui/src/components/import-dataset/styles.scss @@ -17,6 +17,10 @@ color: $text-color-secondary; } +.cvat-modal-import-switch-use-default-storage { + display: table-cell; +} + .cvat-modal-import-dataset-status .ant-modal-body { display: flex; align-items: center; diff --git a/cvat-ui/src/components/jobs-page/job-card.tsx b/cvat-ui/src/components/jobs-page/job-card.tsx index 836e629b..6754fa71 100644 --- a/cvat-ui/src/components/jobs-page/job-card.tsx +++ b/cvat-ui/src/components/jobs-page/job-card.tsx @@ -1,8 +1,10 @@ // Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; import Card from 'antd/lib/card'; import Empty from 'antd/lib/empty'; @@ -12,8 +14,8 @@ import Dropdown from 'antd/lib/dropdown'; import Menu from 'antd/lib/menu'; // eslint-disable-next-line import/no-extraneous-dependencies import { MenuInfo } from 'rc-menu/lib/interface'; - import { useCardHeightHOC } from 'utils/hooks'; +import { exportActions } from 'actions/export-actions'; const useCardHeight = useCardHeightHOC({ containerClassName: 'cvat-jobs-page', @@ -28,6 +30,7 @@ interface Props { } function JobCardComponent(props: Props): JSX.Element { + const dispatch = useDispatch(); const { job, preview } = props; const [expanded, setExpanded] = useState(false); const history = useHistory(); @@ -97,6 +100,7 @@ function JobCardComponent(props: Props): JSX.Element { Go to the task Go to the project Go to the bug tracker + dispatch(exportActions.openExportDatasetModal(job))}>Export job
    )} > diff --git a/cvat-ui/src/components/project-page/project-page.tsx b/cvat-ui/src/components/project-page/project-page.tsx index 31bc2c04..98bfaba0 100644 --- a/cvat-ui/src/components/project-page/project-page.tsx +++ b/cvat-ui/src/components/project-page/project-page.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -22,7 +23,6 @@ import { cancelInferenceAsync } from 'actions/models-actions'; import TaskItem from 'components/tasks-page/task-item'; import MoveTaskModal from 'components/move-task-modal/move-task-modal'; import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog'; -import ImportDatasetModal from 'components/import-dataset-modal/import-dataset-modal'; import { SortingComponent, ResourceFilterHOC, defaultVisibility, updateHistoryFromQuery, } from 'components/resource-sorting-filtering'; @@ -241,7 +241,6 @@ export default function ProjectPageComponent(): JSX.Element { - ); } diff --git a/cvat-ui/src/components/projects-page/actions-menu.tsx b/cvat-ui/src/components/projects-page/actions-menu.tsx index 10539bfb..4854bf92 100644 --- a/cvat-ui/src/components/projects-page/actions-menu.tsx +++ b/cvat-ui/src/components/projects-page/actions-menu.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,9 +8,8 @@ import { useDispatch, useSelector } from 'react-redux'; import Modal from 'antd/lib/modal'; import Menu from 'antd/lib/menu'; import { LoadingOutlined } from '@ant-design/icons'; - import { CombinedState } from 'reducers'; -import { deleteProjectAsync, backupProjectAsync } from 'actions/projects-actions'; +import { deleteProjectAsync } from 'actions/projects-actions'; import { exportActions } from 'actions/export-actions'; import { importActions } from 'actions/import-actions'; @@ -21,8 +21,9 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element { const { projectInstance } = props; const dispatch = useDispatch(); - const activeBackups = useSelector((state: CombinedState) => state.projects.activities.backups); - const exportIsActive = projectInstance.id in activeBackups; + const exportBackupIsActive = useSelector((state: CombinedState) => ( + state.export.projects.backup.current[projectInstance.id] + )); const onDeleteProject = useCallback((): void => { Modal.confirm({ @@ -42,16 +43,16 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element { return ( - dispatch(exportActions.openExportModal(projectInstance))}> + dispatch(exportActions.openExportDatasetModal(projectInstance))}> Export dataset - dispatch(importActions.openImportModal(projectInstance))}> + dispatch(importActions.openImportDatasetModal(projectInstance))}> Import dataset dispatch(backupProjectAsync(projectInstance))} - icon={exportIsActive && } + disabled={exportBackupIsActive} + onClick={() => dispatch(exportActions.openExportBackupModal(projectInstance))} + icon={exportBackupIsActive && } > Backup Project diff --git a/cvat-ui/src/components/projects-page/projects-page.tsx b/cvat-ui/src/components/projects-page/projects-page.tsx index a57c3ec2..cbb0cc29 100644 --- a/cvat-ui/src/components/projects-page/projects-page.tsx +++ b/cvat-ui/src/components/projects-page/projects-page.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,12 +8,10 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; import { useDispatch, useSelector } from 'react-redux'; import Spin from 'antd/lib/spin'; - import { CombinedState, Indexable } from 'reducers'; -import { getProjectsAsync, restoreProjectAsync } from 'actions/projects-actions'; +import { getProjectsAsync } from 'actions/projects-actions'; import FeedbackComponent from 'components/feedback/feedback'; import { updateHistoryFromQuery } from 'components/resource-sorting-filtering'; -import ImportDatasetModal from 'components/import-dataset-modal/import-dataset-modal'; import EmptyListComponent from './empty-list'; import TopBarComponent from './top-bar'; import ProjectListComponent from './project-list'; @@ -24,7 +23,7 @@ export default function ProjectsPageComponent(): JSX.Element { const count = useSelector((state: CombinedState) => state.projects.current.length); const query = useSelector((state: CombinedState) => state.projects.gettingQuery); const tasksQuery = useSelector((state: CombinedState) => state.projects.tasksGettingQuery); - const importing = useSelector((state: CombinedState) => state.projects.restoring); + const importing = useSelector((state: CombinedState) => state.import.projects.backup.importing); const [isMounted, setIsMounted] = useState(false); const anySearch = Object.keys(query).some((value: string) => value !== 'page' && (query as any)[value] !== null); @@ -83,7 +82,6 @@ export default function ProjectsPageComponent(): JSX.Element { ); }} query={updatedQuery} - onImportProject={(file: File) => dispatch(restoreProjectAsync(file))} importing={importing} /> { fetching ? ( @@ -92,7 +90,6 @@ export default function ProjectsPageComponent(): JSX.Element { ) : content } - ); } diff --git a/cvat-ui/src/components/projects-page/top-bar.tsx b/cvat-ui/src/components/projects-page/top-bar.tsx index 76f1d219..fc765c18 100644 --- a/cvat-ui/src/components/projects-page/top-bar.tsx +++ b/cvat-ui/src/components/projects-page/top-bar.tsx @@ -1,16 +1,17 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React, { useState, useEffect } from 'react'; import { useHistory } from 'react-router'; +import { useDispatch } from 'react-redux'; import { Row, Col } from 'antd/lib/grid'; import Button from 'antd/lib/button'; import Dropdown from 'antd/lib/dropdown'; import Input from 'antd/lib/input'; import { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons'; -import Upload from 'antd/lib/upload'; - +import { importActions } from 'actions/import-actions'; import { usePrevious } from 'utils/hooks'; import { ProjectsQuery } from 'reducers'; import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering'; @@ -24,7 +25,6 @@ const FilteringComponent = ResourceFilterHOC( ); interface Props { - onImportProject(file: File): void; onApplyFilter(filter: string | null): void; onApplySorting(sorting: string | null): void; onApplySearch(search: string | null): void; @@ -33,8 +33,9 @@ interface Props { } function TopBarComponent(props: Props): JSX.Element { + const dispatch = useDispatch(); const { - importing, query, onApplyFilter, onApplySorting, onApplySearch, onImportProject, + importing, query, onApplyFilter, onApplySorting, onApplySearch, } = props; const [visibility, setVisibility] = useState(defaultVisibility); const prevImporting = usePrevious(importing); @@ -101,26 +102,16 @@ function TopBarComponent(props: Props): JSX.Element { > Create a new project - { - onImportProject(file); - return false; - }} - className='cvat-import-project' + - + Create from backup + {importing && } + )} > diff --git a/cvat-ui/src/components/select-cloud-storage/select-cloud-storage.tsx b/cvat-ui/src/components/select-cloud-storage/select-cloud-storage.tsx new file mode 100644 index 00000000..e4a220a0 --- /dev/null +++ b/cvat-ui/src/components/select-cloud-storage/select-cloud-storage.tsx @@ -0,0 +1,137 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useEffect, useState } from 'react'; +import Form from 'antd/lib/form'; +import notification from 'antd/lib/notification'; +import AutoComplete from 'antd/lib/auto-complete'; +import Input from 'antd/lib/input'; +import { debounce } from 'lodash'; +import { CloudStorage } from 'reducers'; +import { AzureProvider, GoogleCloudProvider, S3Provider } from 'icons'; +import { ProviderType } from 'utils/enums'; +import { getCore } from 'cvat-core-wrapper'; + +export interface Props { + searchPhrase: string; + cloudStorage: CloudStorage | null; + name?: string[]; + setSearchPhrase: (searchPhrase: string) => void; + onSelectCloudStorage: (cloudStorageId: number | null) => void; +} + +async function searchCloudStorages(filter: Record): Promise { + try { + const data = await getCore().cloudStorages.get(filter); + return data; + } catch (error) { + notification.error({ + message: 'Could not fetch a list of cloud storages', + description: error.toString(), + }); + } + + return []; +} + +const searchCloudStoragesWrapper = debounce((phrase, setList) => { + const filter = { + filter: JSON.stringify({ + and: [{ + '==': [{ var: 'display_name' }, phrase], + }], + }), + }; + searchCloudStorages(filter).then((list) => { + setList(list); + }); +}, 500); + +function SelectCloudStorage(props: Props): JSX.Element { + const { + searchPhrase, cloudStorage, name, setSearchPhrase, onSelectCloudStorage, + } = props; + const [initialList, setInitialList] = useState([]); + const [list, setList] = useState([]); + + useEffect(() => { + searchCloudStorages({}).then((data) => { + setInitialList(data); + if (!list.length) { + setList(data); + } + }); + }, []); + + useEffect(() => { + if (!searchPhrase) { + setList(initialList); + } else { + searchCloudStoragesWrapper(searchPhrase, setList); + } + }, [searchPhrase, initialList]); + + const onBlur = (): void => { + if (!searchPhrase && cloudStorage) { + onSelectCloudStorage(null); + } else if (searchPhrase) { + const potentialStorages = list.filter((_cloudStorage) => _cloudStorage.displayName.includes(searchPhrase)); + if (potentialStorages.length === 1) { + const potentialStorage = potentialStorages[0]; + setSearchPhrase(potentialStorage.displayName); + // eslint-disable-next-line prefer-destructuring + potentialStorage.manifestPath = potentialStorage.manifests[0]; + onSelectCloudStorage(potentialStorage); + } + } + }; + + return ( + + { + setSearchPhrase(phrase); + }} + options={list.map((_cloudStorage) => ({ + value: _cloudStorage.id.toString(), + label: ( + + {_cloudStorage.providerType === ProviderType.AWS_S3_BUCKET && } + {_cloudStorage.providerType === ProviderType.AZURE_CONTAINER && } + { + _cloudStorage.providerType === ProviderType.GOOGLE_CLOUD_STORAGE && + + } + {_cloudStorage.displayName} + + ), + }))} + onSelect={(value: string) => { + const selectedCloudStorage = + list.filter((_cloudStorage: CloudStorage) => _cloudStorage.id === +value)[0] || null; + // eslint-disable-next-line prefer-destructuring + [selectedCloudStorage.manifestPath] = selectedCloudStorage.manifests; + onSelectCloudStorage(selectedCloudStorage); + setSearchPhrase(selectedCloudStorage?.displayName || ''); + }} + allowClear + > + + + + ); +} + +export default React.memo(SelectCloudStorage); diff --git a/cvat-ui/src/components/storage/source-storage-field.tsx b/cvat-ui/src/components/storage/source-storage-field.tsx new file mode 100644 index 00000000..9fda4561 --- /dev/null +++ b/cvat-ui/src/components/storage/source-storage-field.tsx @@ -0,0 +1,52 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import { StorageData } from 'cvat-core-wrapper'; +import { StorageLocation } from 'reducers'; +import StorageWithSwitchField from './storage-with-switch-field'; + +export interface Props { + instanceId: number | null; + locationValue: StorageLocation; + switchDescription?: string; + switchHelpMessage?: string; + storageDescription?: string; + useDefaultStorage?: boolean | null; + onChangeLocationValue?: (value: StorageLocation) => void; + onChangeStorage?: (values: StorageData) => void; + onChangeUseDefaultStorage?: (value: boolean) => void; +} + +export default function SourceStorageField(props: Props): JSX.Element { + const { + instanceId, + switchDescription, + switchHelpMessage, + storageDescription, + useDefaultStorage, + locationValue, + onChangeUseDefaultStorage, + onChangeStorage, + onChangeLocationValue, + } = props; + + return ( + + ); +} diff --git a/cvat-ui/src/components/storage/storage-field.tsx b/cvat-ui/src/components/storage/storage-field.tsx new file mode 100644 index 00000000..c9ebf2a0 --- /dev/null +++ b/cvat-ui/src/components/storage/storage-field.tsx @@ -0,0 +1,83 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useEffect, useState } from 'react'; +import Select from 'antd/lib/select'; +import Form from 'antd/lib/form'; +import { CloudStorage, StorageLocation } from 'reducers'; +import SelectCloudStorage from 'components/select-cloud-storage/select-cloud-storage'; + +import { StorageData } from 'cvat-core-wrapper'; + +const { Option } = Select; + +export interface Props { + locationName: string[]; + selectCloudStorageName: string[]; + locationValue: StorageLocation; + onChangeLocationValue?: (value: StorageLocation) => void; + onChangeStorage?: (value: StorageData) => void; +} + +export default function StorageField(props: Props): JSX.Element { + const { + locationName, + selectCloudStorageName, + locationValue, + onChangeStorage, + onChangeLocationValue, + } = props; + const [cloudStorage, setCloudStorage] = useState(null); + const [potentialCloudStorage, setPotentialCloudStorage] = useState(''); + + function renderCloudStorage(): JSX.Element { + return ( + { + setPotentialCloudStorage(cs); + }} + name={selectCloudStorageName} + onSelectCloudStorage={(_cloudStorage: CloudStorage | null) => setCloudStorage(_cloudStorage)} + /> + ); + } + + useEffect(() => { + if (locationValue === StorageLocation.LOCAL) { + setPotentialCloudStorage(''); + } + }, [locationValue]); + + useEffect(() => { + if (onChangeStorage) { + onChangeStorage({ + location: locationValue, + cloudStorageId: cloudStorage?.id ? parseInt(cloudStorage?.id, 10) : undefined, + }); + } + }, [cloudStorage, locationValue]); + + return ( + <> + + + + {locationValue === StorageLocation.CLOUD_STORAGE && renderCloudStorage()} + + ); +} diff --git a/cvat-ui/src/components/storage/storage-with-switch-field.tsx b/cvat-ui/src/components/storage/storage-with-switch-field.tsx new file mode 100644 index 00000000..6ff6ddda --- /dev/null +++ b/cvat-ui/src/components/storage/storage-with-switch-field.tsx @@ -0,0 +1,104 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import Form from 'antd/lib/form'; +import Text from 'antd/lib/typography/Text'; +import Space from 'antd/lib/space'; +import Switch from 'antd/lib/switch'; +import Tooltip from 'antd/lib/tooltip'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import CVATTooltip from 'components/common/cvat-tooltip'; +import { StorageData } from 'cvat-core-wrapper'; +import { StorageLocation } from 'reducers'; +import StorageField from './storage-field'; + +export interface Props { + instanceId: number | null; + storageName: string; + storageLabel: string; + switchName: string; + locationValue: StorageLocation; + switchDescription?: string; + switchHelpMessage?: string; + storageDescription?: string; + useDefaultStorage?: boolean | null; + onChangeLocationValue?: (value: StorageLocation) => void; + onChangeStorage?: (values: StorageData) => void; + onChangeUseDefaultStorage?: (value: boolean) => void; +} + +export default function StorageWithSwitchField(props: Props): JSX.Element { + const { + instanceId, + storageName, + storageLabel, + switchName, + switchDescription, + switchHelpMessage, + storageDescription, + useDefaultStorage, + locationValue, + onChangeUseDefaultStorage, + onChangeStorage, + onChangeLocationValue, + } = props; + + return ( + <> + { + !!instanceId && ( + + + { + if (onChangeUseDefaultStorage) { + onChangeUseDefaultStorage(value); + } + }} + /> + + {switchDescription} + {(switchHelpMessage) ? ( + + + + ) : null} + + ) + } + { + (!instanceId || !useDefaultStorage) && ( + + + {storageLabel} + + + + + + )} + > + + + ) + } + + ); +} diff --git a/cvat-ui/src/components/storage/styles.scss b/cvat-ui/src/components/storage/styles.scss new file mode 100644 index 00000000..6e87c818 --- /dev/null +++ b/cvat-ui/src/components/storage/styles.scss @@ -0,0 +1,10 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-question-circle-filled-icon { + font-size: $grid-unit-size * 14; + opacity: 0.5; +} diff --git a/cvat-ui/src/components/storage/target-storage-field.tsx b/cvat-ui/src/components/storage/target-storage-field.tsx new file mode 100644 index 00000000..b403cae4 --- /dev/null +++ b/cvat-ui/src/components/storage/target-storage-field.tsx @@ -0,0 +1,52 @@ +// (Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import { StorageLocation } from 'reducers'; +import { StorageData } from 'cvat-core-wrapper'; +import StorageWithSwitchField from './storage-with-switch-field'; + +export interface Props { + instanceId: number | null; + locationValue: StorageLocation; + switchDescription?: string; + switchHelpMessage?: string; + storageDescription?: string; + useDefaultStorage?: boolean | null; + onChangeLocationValue?: (value: StorageLocation) => void; + onChangeStorage?: (values: StorageData) => void; + onChangeUseDefaultStorage?: (value: boolean) => void; +} + +export default function TargetStorageField(props: Props): JSX.Element { + const { + instanceId, + locationValue, + switchDescription, + switchHelpMessage, + storageDescription, + useDefaultStorage, + onChangeLocationValue, + onChangeUseDefaultStorage, + onChangeStorage, + } = props; + + return ( + + ); +} diff --git a/cvat-ui/src/components/tasks-page/tasks-page.tsx b/cvat-ui/src/components/tasks-page/tasks-page.tsx index c4d0e4c3..c1b58f8c 100644 --- a/cvat-ui/src/components/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/components/tasks-page/tasks-page.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -17,7 +18,7 @@ import { TasksQuery, Indexable } from 'reducers'; import FeedbackComponent from 'components/feedback/feedback'; import { updateHistoryFromQuery } from 'components/resource-sorting-filtering'; import TaskListContainer from 'containers/tasks-page/tasks-list'; -import { getTasksAsync, hideEmptyTasks, importTaskAsync } from 'actions/tasks-actions'; +import { getTasksAsync, hideEmptyTasks } from 'actions/tasks-actions'; import TopBar from './top-bar'; import EmptyListComponent from './empty-list'; @@ -139,7 +140,6 @@ function TasksPageComponent(props: Props): JSX.Element { ); }} query={updatedQuery} - onImportTask={(file: File) => dispatch(importTaskAsync(file))} importing={importing} /> { fetching ? ( diff --git a/cvat-ui/src/components/tasks-page/top-bar.tsx b/cvat-ui/src/components/tasks-page/top-bar.tsx index cdd9fcb4..213a29ef 100644 --- a/cvat-ui/src/components/tasks-page/top-bar.tsx +++ b/cvat-ui/src/components/tasks-page/top-bar.tsx @@ -1,16 +1,18 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React, { useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; + import { Row, Col } from 'antd/lib/grid'; import Dropdown from 'antd/lib/dropdown'; import { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons'; import Button from 'antd/lib/button'; -import Upload from 'antd/lib/upload'; import Input from 'antd/lib/input'; - +import { importActions } from 'actions/import-actions'; import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering'; import { TasksQuery } from 'reducers'; import { usePrevious } from 'utils/hooks'; @@ -23,7 +25,6 @@ const FilteringComponent = ResourceFilterHOC( ); interface VisibleTopBarProps { - onImportTask(file: File): void; onApplyFilter(filter: string | null): void; onApplySorting(sorting: string | null): void; onApplySearch(search: string | null): void; @@ -32,8 +33,9 @@ interface VisibleTopBarProps { } export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element { + const dispatch = useDispatch(); const { - importing, query, onApplyFilter, onApplySorting, onApplySearch, onImportTask, + importing, query, onApplyFilter, onApplySorting, onApplySearch, } = props; const [visibility, setVisibility] = useState(defaultVisibility); const history = useHistory(); @@ -99,26 +101,16 @@ export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element > Create a new task - { - onImportTask(file); - return false; - }} - className='cvat-import-task' + - + Create from backup + {importing && } + )} > diff --git a/cvat-ui/src/containers/actions-menu/actions-menu.tsx b/cvat-ui/src/containers/actions-menu/actions-menu.tsx index c401f7fb..34bd5bff 100644 --- a/cvat-ui/src/containers/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/containers/actions-menu/actions-menu.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,18 +7,16 @@ import React from 'react'; import { connect } from 'react-redux'; // eslint-disable-next-line import/no-extraneous-dependencies import { MenuInfo } from 'rc-menu/lib/interface'; - import ActionsMenuComponent, { Actions } from 'components/actions-menu/actions-menu'; import { CombinedState } from 'reducers'; import { modelsActions } from 'actions/models-actions'; import { - loadAnnotationsAsync, deleteTaskAsync, - exportTaskAsync, switchMoveTaskModalVisible, } from 'actions/tasks-actions'; import { exportActions } from 'actions/export-actions'; +import { importActions } from 'actions/import-actions'; interface OwnProps { taskInstance: any; @@ -25,17 +24,15 @@ interface OwnProps { interface StateToProps { annotationFormats: any; - loadActivity: string | null; inferenceIsActive: boolean; - exportIsActive: boolean; + backupIsActive: boolean; } interface DispatchToProps { - loadAnnotations: (taskInstance: any, loader: any, file: File) => void; - showExportModal: (taskInstance: any) => void; - deleteTask: (taskInstance: any) => void; + showExportModal: (taskInstance: any, resource: 'dataset' | 'backup') => void; + showImportModal: (taskInstance: any) => void; openRunModelWindow: (taskInstance: any) => void; - exportTask: (taskInstance: any) => void; + deleteTask: (taskInstance: any) => void; openMoveTaskToProjectWindow: (taskInstance: any) => void; } @@ -46,26 +43,26 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { const { formats: { annotationFormats }, - tasks: { - activities: { loads, backups }, - }, } = state; return { - loadActivity: tid in loads ? loads[tid] : null, annotationFormats, inferenceIsActive: tid in state.models.inferences, - exportIsActive: tid in backups, + backupIsActive: state.export.tasks.backup.current[tid], }; } function mapDispatchToProps(dispatch: any): DispatchToProps { return { - loadAnnotations: (taskInstance: any, loader: any, file: File): void => { - dispatch(loadAnnotationsAsync(taskInstance, loader, file)); + showExportModal: (taskInstance: any, resource: 'dataset' | 'backup'): void => { + if (resource === 'dataset') { + dispatch(exportActions.openExportDatasetModal(taskInstance)); + } else { + dispatch(exportActions.openExportBackupModal(taskInstance)); + } }, - showExportModal: (taskInstance: any): void => { - dispatch(exportActions.openExportModal(taskInstance)); + showImportModal: (taskInstance: any): void => { + dispatch(importActions.openImportDatasetModal(taskInstance)); }, deleteTask: (taskInstance: any): void => { dispatch(deleteTaskAsync(taskInstance)); @@ -73,9 +70,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { openRunModelWindow: (taskInstance: any): void => { dispatch(modelsActions.showRunModelDialog(taskInstance)); }, - exportTask: (taskInstance: any): void => { - dispatch(exportTaskAsync(taskInstance)); - }, openMoveTaskToProjectWindow: (taskId: number): void => { dispatch(switchMoveTaskModalVisible(true, taskId)); }, @@ -86,21 +80,19 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): const { taskInstance, annotationFormats: { loaders, dumpers }, - loadActivity, inferenceIsActive, - exportIsActive, - loadAnnotations, + backupIsActive, showExportModal, + showImportModal, deleteTask, openRunModelWindow, - exportTask, openMoveTaskToProjectWindow, } = props; - const onClickMenu = (params: MenuInfo): void => { + const onClickMenu = (params: MenuInfo): void | JSX.Element => { const [action] = params.keyPath; if (action === Actions.EXPORT_TASK_DATASET) { - showExportModal(taskInstance); + showExportModal(taskInstance, 'dataset'); } else if (action === Actions.DELETE_TASK) { deleteTask(taskInstance); } else if (action === Actions.OPEN_BUG_TRACKER) { @@ -108,17 +100,12 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): window.open(`${taskInstance.bugTracker}`, '_blank'); } else if (action === Actions.RUN_AUTO_ANNOTATION) { openRunModelWindow(taskInstance); - } else if (action === Actions.EXPORT_TASK) { - exportTask(taskInstance); + } else if (action === Actions.BACKUP_TASK) { + showExportModal(taskInstance, 'backup'); } else if (action === Actions.MOVE_TASK_TO_PROJECT) { openMoveTaskToProjectWindow(taskInstance.id); - } - }; - - const onUploadAnnotations = (format: string, file: File): void => { - const [loader] = loaders.filter((_loader: any): boolean => _loader.name === format); - if (loader && file) { - loadAnnotations(taskInstance, loader, file); + } else if (action === Actions.LOAD_TASK_ANNO) { + showImportModal(taskInstance); } }; @@ -129,12 +116,10 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): bugTracker={taskInstance.bugTracker} loaders={loaders} dumpers={dumpers} - loadActivity={loadActivity} inferenceIsActive={inferenceIsActive} onClickMenu={onClickMenu} - onUploadAnnotations={onUploadAnnotations} taskDimension={taskInstance.dimension} - exportIsActive={exportIsActive} + backupIsActive={backupIsActive} /> ); } 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 f5d64f50..0e9908c7 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 @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -12,26 +13,24 @@ import { CombinedState, JobStage } from 'reducers'; import AnnotationMenuComponent, { Actions } from 'components/annotation-page/top-bar/annotation-menu'; import { updateJobAsync } from 'actions/tasks-actions'; import { - uploadJobAnnotationsAsync, saveAnnotationsAsync, setForceExitAnnotationFlag as setForceExitAnnotationFlagAction, removeAnnotationsAsync as removeAnnotationsAsyncAction, } from 'actions/annotation-actions'; import { exportActions } from 'actions/export-actions'; +import { importActions } from 'actions/import-actions'; import { getCore } from 'cvat-core-wrapper'; const core = getCore(); interface StateToProps { - annotationFormats: any; jobInstance: any; stopFrame: number; - loadActivity: string | null; } interface DispatchToProps { - loadAnnotations(job: any, loader: any, file: File): void; - showExportModal(jobInstance: any): void; + showExportModal: (jobInstance: any) => void; + showImportModal: (jobInstance: any) => void; removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly: boolean): void; setForceExitAnnotationFlag(forceExit: boolean): void; saveAnnotations(jobInstance: any, afterSave?: () => void): void; @@ -41,36 +40,26 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState): StateToProps { const { annotation: { - activities: { loads: jobLoads }, job: { instance: jobInstance, instance: { stopFrame }, }, }, - formats: { annotationFormats }, - tasks: { - activities: { loads }, - }, } = state; - const taskID = jobInstance.taskId; - const jobID = jobInstance.id; - return { - loadActivity: taskID in loads || jobID in jobLoads ? loads[taskID] || jobLoads[jobID] : null, jobInstance, stopFrame, - annotationFormats, }; } function mapDispatchToProps(dispatch: any): DispatchToProps { return { - loadAnnotations(job: any, loader: any, file: File): void { - dispatch(uploadJobAnnotationsAsync(job, loader, file)); - }, showExportModal(jobInstance: any): void { - dispatch(exportActions.openExportModal(jobInstance)); + dispatch(exportActions.openExportDatasetModal(jobInstance)); + }, + showImportModal(jobInstance: any): void { + dispatch(importActions.openImportDatasetModal(jobInstance)); }, removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean) { dispatch(removeAnnotationsAsyncAction(startnumber, endnumber, delTrackKeyframesOnly)); @@ -93,27 +82,18 @@ function AnnotationMenuContainer(props: Props): JSX.Element { const { jobInstance, stopFrame, - annotationFormats: { loaders, dumpers }, history, - loadActivity, - loadAnnotations, showExportModal, + showImportModal, removeAnnotations, setForceExitAnnotationFlag, saveAnnotations, updateJob, } = props; - const onUploadAnnotations = (format: string, file: File): void => { - const [loader] = loaders.filter((_loader: any): boolean => _loader.name === format); - if (loader && file) { - loadAnnotations(jobInstance, loader, file); - } - }; - const onClickMenu = (params: MenuInfo): void => { const [action] = params.keyPath; - if (action === Actions.EXPORT_TASK_DATASET) { + if (action === Actions.EXPORT_JOB_DATASET) { showExportModal(jobInstance); } else if (action === Actions.RENEW_JOB) { jobInstance.state = core.enums.JobState.NEW; @@ -131,16 +111,14 @@ function AnnotationMenuContainer(props: Props): JSX.Element { [, jobInstance.state] = action.split(':'); updateJob(jobInstance); window.location.reload(); + } else if (action === Actions.LOAD_JOB_ANNO) { + showImportModal(jobInstance); } }; return ( !task.instance.jobs.length).length : 0, - importing: state.tasks.importing, + importing: state.import.tasks.backup.importing, }; } diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index c9585d1a..888bf20a 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -8,6 +8,7 @@ import { Label, Attribute, RawAttribute, RawLabel, } from 'cvat-core/src/labels'; import { ShapeType } from 'cvat-core/src/enums'; +import { Storage, StorageData } from 'cvat-core/src/storage'; const cvat: any = _cvat; @@ -26,9 +27,11 @@ export { Label, Attribute, ShapeType, + Storage, }; export type { RawAttribute, RawLabel, + StorageData, }; diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 426def56..ada22763 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -937,19 +938,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }; } case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS: { - const { states, job, history } = action.payload; - const { loads } = state.activities; - - delete loads[job.id]; + const { states, history } = action.payload; return { ...state, - activities: { - ...state.activities, - loads: { - ...loads, - }, - }, annotations: { ...state.annotations, history, diff --git a/cvat-ui/src/reducers/export-reducer.ts b/cvat-ui/src/reducers/export-reducer.ts index 84c9d95d..bd944e4e 100644 --- a/cvat-ui/src/reducers/export-reducer.ts +++ b/cvat-ui/src/reducers/export-reducer.ts @@ -1,66 +1,180 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { ExportActions, ExportActionTypes } from 'actions/export-actions'; -import { getCore } from 'cvat-core-wrapper'; +import { omit } from 'lodash'; import deepCopy from 'utils/deep-copy'; import { ExportState } from '.'; - -const core = getCore(); +import { defineActititiesField } from './import-reducer'; const defaultState: ExportState = { - tasks: {}, - projects: {}, - instance: null, - modalVisible: false, + projects: { + dataset: { + current: {}, + modalInstance: null, + }, + backup: { + modalInstance: null, + current: {}, + }, + }, + tasks: { + dataset: { + current: {}, + modalInstance: null, + }, + backup: { + modalInstance: null, + current: {}, + }, + }, + jobs: { + dataset: { + current: {}, + modalInstance: null, + }, + }, + instanceType: null, }; export default (state: ExportState = defaultState, action: ExportActions): ExportState => { switch (action.type) { - case ExportActionTypes.OPEN_EXPORT_MODAL: + case ExportActionTypes.OPEN_EXPORT_DATASET_MODAL: { + const { instance } = action.payload; + const activitiesField = defineActititiesField(instance); + return { ...state, - modalVisible: true, - instance: action.payload.instance, + [activitiesField]: { + ...state[activitiesField], + dataset: { + ...state[activitiesField].dataset, + modalInstance: instance, + }, + }, + instanceType: activitiesField + .slice(0, activitiesField.length - 1) as 'project' | 'task' | 'job', }; - case ExportActionTypes.CLOSE_EXPORT_MODAL: + } + case ExportActionTypes.CLOSE_EXPORT_DATASET_MODAL: { + const { instance } = action.payload; + const activitiesField = defineActititiesField(instance); + return { ...state, - modalVisible: false, - instance: null, + [activitiesField]: { + ...state[activitiesField], + dataset: { + ...state[activitiesField].dataset, + modalInstance: null, + }, + }, + instanceType: null, }; + } case ExportActionTypes.EXPORT_DATASET: { const { instance, format } = action.payload; - const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks); - const instanceId = instance instanceof core.classes.Project || - instance instanceof core.classes.Task ? instance.id : instance.taskId; - - activities[instanceId] = - instanceId in activities && !activities[instanceId].includes(format) ? - [...activities[instanceId], format] : - activities[instanceId] || [format]; + const field = defineActititiesField(instance) as 'projects' | 'tasks' | 'jobs'; return { ...state, - ...(instance instanceof core.classes.Project ? { projects: activities } : { tasks: activities }), + [field]: { + ...state[field], + dataset: { + ...state[field].dataset, + current: { + ...state[field].dataset.current, + [instance.id]: !state[field].dataset.current[instance.id] ? [format] : + [...state[field].dataset.current[instance.id], format], + }, + }, + }, }; } case ExportActionTypes.EXPORT_DATASET_FAILED: case ExportActionTypes.EXPORT_DATASET_SUCCESS: { const { instance, format } = action.payload; - const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks); - const instanceId = instance instanceof core.classes.Project || - instance instanceof core.classes.Task ? instance.id : instance.taskId; + const field: 'projects' | 'tasks' | 'jobs' = defineActititiesField(instance); + const activities = deepCopy(state[field]); - activities[instanceId] = activities[instanceId].filter( + activities.dataset.current[instance.id] = activities.dataset.current[instance.id].filter( (exporterName: string): boolean => exporterName !== format, ); return { ...state, - ...(instance instanceof core.classes.Project ? { projects: activities } : { tasks: activities }), + [field]: activities, + }; + } + case ExportActionTypes.OPEN_EXPORT_BACKUP_MODAL: { + const { instance } = action.payload; + const field = defineActititiesField(instance) as 'projects' | 'tasks'; + + return { + ...state, + [field]: { + ...state[field], + backup: { + ...state[field].backup, + modalInstance: instance, + }, + }, + instanceType: field + .slice(0, field.length - 1) as 'project' | 'task', + }; + } + case ExportActionTypes.CLOSE_EXPORT_BACKUP_MODAL: { + const { instance } = action.payload; + const field = defineActititiesField(instance) as 'projects' | 'tasks'; + + return { + ...state, + [field]: { + ...state[field], + backup: { + ...state[field].backup, + modalInstance: null, + }, + }, + instanceType: null, + }; + } + case ExportActionTypes.EXPORT_BACKUP: { + const { instance } = action.payload; + const field = defineActititiesField(instance) as 'projects' | 'tasks'; + + return { + ...state, + [field]: { + ...state[field], + backup: { + ...state[field].backup, + current: { + ...state[field].backup.current, + [instance.id]: true, + }, + }, + }, + }; + } + case ExportActionTypes.EXPORT_BACKUP_FAILED: + case ExportActionTypes.EXPORT_BACKUP_SUCCESS: { + const { instance } = action.payload; + + const field = defineActititiesField(instance) as 'projects' | 'tasks'; + + return { + ...state, + [field]: { + ...state[field], + backup: { + ...state[field].backup, + current: omit(state[field].backup, instance.id), + }, + }, }; } default: diff --git a/cvat-ui/src/reducers/import-reducer.ts b/cvat-ui/src/reducers/import-reducer.ts index 17933501..e91ad0cb 100644 --- a/cvat-ui/src/reducers/import-reducer.ts +++ b/cvat-ui/src/reducers/import-reducer.ts @@ -1,58 +1,223 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT +import { omit } from 'lodash'; import { ImportActions, ImportActionTypes } from 'actions/import-actions'; - +import { getCore } from 'cvat-core-wrapper'; import { ImportState } from '.'; +const core = getCore(); + +const defaultProgress = 0.0; + +export function defineActititiesField(instance: any): 'projects' | 'tasks' | 'jobs' { + if (instance instanceof core.classes.Project) { + return 'projects'; + } + if (instance instanceof core.classes.Task) { + return 'tasks'; + } + return 'jobs'; +} + const defaultState: ImportState = { - progress: 0.0, - status: '', - instance: null, - importingId: null, - modalVisible: false, + projects: { + dataset: { + modalInstance: null, + current: {}, + }, + backup: { + modalVisible: false, + importing: false, + }, + }, + tasks: { + dataset: { + modalInstance: null, + current: {}, + }, + backup: { + modalVisible: false, + importing: false, + }, + }, + jobs: { + dataset: { + modalInstance: null, + current: {}, + }, + }, + instanceType: null, }; export default (state: ImportState = defaultState, action: ImportActions): ImportState => { switch (action.type) { - case ImportActionTypes.OPEN_IMPORT_MODAL: + case ImportActionTypes.OPEN_IMPORT_DATASET_MODAL: { + const { instance } = action.payload; + const activitiesField = defineActititiesField(instance); + return { ...state, - modalVisible: true, - instance: action.payload.instance, + [activitiesField]: { + ...state[activitiesField], + dataset: { + ...state[activitiesField].dataset, + modalInstance: instance, + }, + }, + instanceType: activitiesField + .slice(0, activitiesField.length - 1) as 'project' | 'task' | 'job', }; - case ImportActionTypes.CLOSE_IMPORT_MODAL: { + } + case ImportActionTypes.CLOSE_IMPORT_DATASET_MODAL: { + const { instance } = action.payload; + const activitiesField = defineActititiesField(instance); + return { ...state, - modalVisible: false, - instance: null, + [activitiesField]: { + ...state[activitiesField], + dataset: { + ...state[activitiesField].dataset, + modalInstance: null, + }, + }, + instanceType: null, }; } case ImportActionTypes.IMPORT_DATASET: { - const { id } = action.payload; + const { format, instance } = action.payload; + + const activitiesField = defineActititiesField(instance); + let updatedActivity: { + format: string; + status?: string; + progress?: number; + } = { format }; + if (activitiesField === 'projects') { + updatedActivity = { + ...updatedActivity, + status: 'The file is being uploaded to the server', + progress: defaultProgress, + }; + } return { ...state, - importingId: id, - status: 'The file is being uploaded to the server', + [activitiesField]: { + ...state[activitiesField], + dataset: { + ...state[activitiesField].dataset, + current: { + ...state[activitiesField].dataset.current, + [instance.id]: updatedActivity, + }, + }, + }, }; } case ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS: { - const { progress, status } = action.payload; + const { progress, status, instance } = action.payload; + + const activitiesField = defineActititiesField(instance); return { ...state, - progress, - status, + [activitiesField]: { + ...state[activitiesField], + dataset: { + ...state[activitiesField].dataset, + current: { + ...state[activitiesField].dataset.current, + [instance.id]: { + ...state[activitiesField].dataset.current[instance.id] as Record, + progress, + status, + }, + }, + }, + }, }; } case ImportActionTypes.IMPORT_DATASET_FAILED: case ImportActionTypes.IMPORT_DATASET_SUCCESS: { + const { instance } = action.payload; + const activitiesField = defineActititiesField(instance); + const { current } = state[activitiesField].dataset; + + return { + ...state, + [activitiesField]: { + ...state[activitiesField], + dataset: { + ...state[activitiesField].dataset, + current: omit(current, instance.id), + }, + }, + }; + } + case ImportActionTypes.OPEN_IMPORT_BACKUP_MODAL: { + const { instanceType } = action.payload; + const field = `${instanceType}s` as 'projects' | 'tasks'; + + return { + ...state, + [field]: { + ...state[field], + backup: { + modalVisible: true, + importing: false, + }, + }, + instanceType, + }; + } + case ImportActionTypes.CLOSE_IMPORT_BACKUP_MODAL: { + const { instanceType } = action.payload; + const field = `${instanceType}s` as 'projects' | 'tasks'; + + return { + ...state, + [field]: { + ...state[field], + backup: { + ...state[field].backup, + modalVisible: false, + }, + }, + instanceType: null, + }; + } + case ImportActionTypes.IMPORT_BACKUP: { + const { instanceType } = state; + const field = `${instanceType}s` as 'projects' | 'tasks'; + + return { + ...state, + [field]: { + ...state[field], + backup: { + ...state[field].backup, + importing: true, + }, + }, + }; + } + case ImportActionTypes.IMPORT_BACKUP_FAILED: + case ImportActionTypes.IMPORT_BACKUP_SUCCESS: { + const { instanceType } = action.payload; + const field = `${instanceType}s` as 'projects' | 'tasks'; + return { ...state, - progress: defaultState.progress, - status: defaultState.status, - importingId: null, + [`${instanceType}s`]: { + ...state[field], + backup: { + ...state[field].backup, + importing: false, + }, + }, }; } default: diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index cdba4e34..5e28168f 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -51,11 +52,7 @@ export interface ProjectsState { deletes: { [projectId: number]: boolean; // deleted (deleting if in dictionary) }; - backups: { - [projectId: number]: boolean; - } }; - restoring: boolean; } export interface TasksQuery { @@ -88,7 +85,6 @@ export interface JobsState { } export interface TasksState { - importing: boolean; initialized: boolean; fetching: boolean; updating: boolean; @@ -101,10 +97,6 @@ export interface TasksState { count: number; current: Task[]; activities: { - loads: { - // only one loading simultaneously - [tid: number]: string; // loader name - }; deletes: { [tid: number]: boolean; // deleted (deleting if in dictionary) }; @@ -113,9 +105,6 @@ export interface TasksState { status: string; error: string; }; - backups: { - [tid: number]: boolean; - }; jobUpdates: { [jid: number]: boolean, }; @@ -123,22 +112,83 @@ export interface TasksState { } export interface ExportState { + projects: { + dataset: { + current: { + [id: number]: string[]; + }; + modalInstance: any | null; + }; + backup: { + current: { + [id: number]: boolean; + }; + modalInstance: any | null; + }; + }; tasks: { - [tid: number]: string[]; + dataset: { + current: { + [id: number]: string[]; + }; + modalInstance: any | null; + }; + backup: { + current: { + [id: number]: boolean; + }; + modalInstance: any | null; + }; }; - projects: { - [pid: number]: string[]; + jobs: { + dataset: { + current: { + [id: number]: string[]; + }; + modalInstance: any | null; + }; }; - instance: any; - modalVisible: boolean; + instanceType: 'project' | 'task' | 'job' | null; } export interface ImportState { - importingId: number | null; - progress: number; - status: string; - instance: any; - modalVisible: boolean; + projects: { + dataset: { + modalInstance: any | null; + current: { + [id: number]: { + format: string; + progress: number; + status: string; + }; + }; + }; + backup: { + modalVisible: boolean; + importing: boolean; + } + }; + tasks: { + dataset: { + modalInstance: any | null; + current: { + [id: number]: string; + }; + }; + backup: { + modalVisible: boolean; + importing: boolean; + } + }; + jobs: { + dataset: { + modalInstance: any | null; + current: { + [id: number]: string; + }; + }; + }; + instanceType: 'project' | 'task' | 'job' | null; } export interface FormatsState { @@ -438,10 +488,12 @@ export interface NotificationsState { exporting: { dataset: null | ErrorState; annotation: null | ErrorState; + backup: null | ErrorState; }; importing: { dataset: null | ErrorState; annotation: null | ErrorState; + backup: null | ErrorState; }; cloudStorages: { creating: null | ErrorState; @@ -478,7 +530,17 @@ export interface NotificationsState { }; projects: { restoringDone: string; - } + }; + exporting: { + dataset: string; + annotation: string; + backup: string; + }; + importing: { + dataset: string; + annotation: string; + backup: string; + }; }; } @@ -740,6 +802,11 @@ export interface ShortcutsState { normalizedKeyMap: Record; } +export enum StorageLocation { + LOCAL = 'local', + CLOUD_STORAGE = 'cloud_storage', +} + export enum ReviewStatus { ACCEPTED = 'accepted', REJECTED = 'rejected', diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index d3cf440c..0bb87a83 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -22,11 +23,8 @@ import { CloudStorageActionTypes } from 'actions/cloud-storage-actions'; import { OrganizationActionsTypes } from 'actions/organization-actions'; import { JobsActionTypes } from 'actions/jobs-actions'; -import { getCore } from 'cvat-core-wrapper'; import { NotificationsState } from '.'; -const core = getCore(); - const defaultState: NotificationsState = { errors: { auth: { @@ -128,10 +126,12 @@ const defaultState: NotificationsState = { exporting: { dataset: null, annotation: null, + backup: null, }, importing: { dataset: null, annotation: null, + backup: null, }, cloudStorages: { creating: null, @@ -169,6 +169,16 @@ const defaultState: NotificationsState = { projects: { restoringDone: '', }, + exporting: { + dataset: '', + annotation: '', + backup: '', + }, + importing: { + dataset: '', + annotation: '', + backup: '', + }, }, }; @@ -353,8 +363,7 @@ export default function (state = defaultState, action: AnyAction): Notifications }; } case ExportActionTypes.EXPORT_DATASET_FAILED: { - const instanceID = action.payload.instance.id; - const instanceType = action.payload.instance instanceof core.classes.Project ? 'project' : 'task'; + const { instance, instanceType } = action.payload; return { ...state, errors: { @@ -364,173 +373,203 @@ export default function (state = defaultState, action: AnyAction): Notifications dataset: { message: 'Could not export dataset for the ' + - `` + - `${instanceType} ${instanceID}`, + `` + + `${instanceType} ${instance.id}`, reason: action.payload.error.toString(), }, }, }, }; } - case ImportActionTypes.IMPORT_DATASET_FAILED: { - const instanceID = action.payload.instance.id; + case ExportActionTypes.EXPORT_DATASET_SUCCESS: { + const { + instance, instanceType, isLocal, resource, + } = action.payload; + const auxiliaryVerb = resource === 'Dataset' ? 'has' : 'have'; return { ...state, - errors: { - ...state.errors, + messages: { + ...state.messages, exporting: { - ...state.errors.exporting, - dataset: { - message: - 'Could not import dataset to the ' + - `` + - `project ${instanceID}`, - reason: action.payload.error.toString(), - }, + ...state.messages.exporting, + dataset: + `${resource} for ${instanceType} ${instance.id} ` + + `${auxiliaryVerb} been ${(isLocal) ? 'downloaded' : 'uploaded'} ` + + `${(isLocal) ? 'locally' : 'to cloud storage'}`, }, }, }; } - case TasksActionTypes.GET_TASKS_FAILED: { + case ExportActionTypes.EXPORT_BACKUP_FAILED: { + const { instance, instanceType } = action.payload; return { ...state, errors: { ...state.errors, - tasks: { - ...state.errors.tasks, - fetching: { - message: 'Could not fetch tasks', + exporting: { + ...state.errors.exporting, + backup: { + message: + `Could not export the ${instanceType} â„–${instance.id}`, reason: action.payload.error.toString(), }, }, }, }; } - case TasksActionTypes.LOAD_ANNOTATIONS_FAILED: { - const taskID = action.payload.task.id; + case ExportActionTypes.EXPORT_BACKUP_SUCCESS: { + const { instance, instanceType, isLocal } = action.payload; return { ...state, - errors: { - ...state.errors, - tasks: { - ...state.errors.tasks, - loading: { - message: - 'Could not upload annotation for the ' + - `task ${taskID}`, - reason: action.payload.error.toString(), - className: 'cvat-notification-notice-load-annotation-failed', - }, + messages: { + ...state.messages, + exporting: { + ...state.messages.exporting, + backup: + `Backup for the ${instanceType} â„–${instance.id} ` + + `has been ${(isLocal) ? 'downloaded' : 'uploaded'} ` + + `${(isLocal) ? 'locally' : 'to cloud storage'}`, }, }, }; } - case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: { - const taskID = action.payload.task.id; + case ImportActionTypes.IMPORT_DATASET_SUCCESS: { + const { instance, resource } = action.payload; + const message = resource === 'annotation' ? + 'Annotations have been loaded to the ' + + `` + + `task ${instance.taskId || instance.id}` : + 'Dataset has been imported to the ' + + `project ${instance.id}`; return { ...state, messages: { ...state.messages, - tasks: { - ...state.messages.tasks, - loadingDone: - 'Annotations have been loaded to the ' + - `task ${taskID}`, + importing: { + ...state.messages.importing, + [resource]: message, }, }, }; } - case TasksActionTypes.UPDATE_TASK_FAILED: { - const taskID = action.payload.task.id; + case ImportActionTypes.IMPORT_DATASET_FAILED: { + const { instance, resource } = action.payload; + const message = resource === 'annotation' ? 'Could not upload annotation for the ' + + `` : + 'Could not import dataset to the ' + + `` + + `project ${instance.id}`; return { ...state, errors: { ...state.errors, - tasks: { - ...state.errors.tasks, - updating: { - message: `Could not update task ${taskID}`, + importing: { + ...state.errors.importing, + dataset: { + message, reason: action.payload.error.toString(), - className: 'cvat-notification-notice-update-task-failed', + className: 'cvat-notification-notice-' + + `${resource === 'annotation' ? 'load-annotation' : 'import-dataset'}-failed`, }, }, }, }; } - case TasksActionTypes.DELETE_TASK_FAILED: { - const { taskID } = action.payload; + case ImportActionTypes.IMPORT_BACKUP_SUCCESS: { + const { instanceId, instanceType } = action.payload; + return { + ...state, + messages: { + ...state.messages, + importing: { + ...state.messages.importing, + backup: + `The ${instanceType} has been restored succesfully. + Click here to open`, + }, + }, + }; + } + case ImportActionTypes.IMPORT_BACKUP_FAILED: { + const { instanceType } = action.payload; return { ...state, errors: { ...state.errors, - tasks: { - ...state.errors.tasks, - deleting: { + importing: { + ...state.errors.importing, + backup: { message: - 'Could not delete the ' + - `task ${taskID}`, + `Could not restore ${instanceType} backup.`, reason: action.payload.error.toString(), - className: 'cvat-notification-notice-delete-task-failed', }, }, }, }; } - case TasksActionTypes.CREATE_TASK_FAILED: { + case TasksActionTypes.GET_TASKS_FAILED: { return { ...state, errors: { ...state.errors, tasks: { ...state.errors.tasks, - creating: { - message: 'Could not create the task', + fetching: { + message: 'Could not fetch tasks', reason: action.payload.error.toString(), - className: 'cvat-notification-notice-create-task-failed', }, }, }, }; } - case TasksActionTypes.EXPORT_TASK_FAILED: { + case TasksActionTypes.UPDATE_TASK_FAILED: { + const taskID = action.payload.task.id; return { ...state, errors: { ...state.errors, tasks: { ...state.errors.tasks, - exporting: { - message: 'Could not export the task', + updating: { + message: `Could not update task ${taskID}`, reason: action.payload.error.toString(), + className: 'cvat-notification-notice-update-task-failed', }, }, }, }; } - case TasksActionTypes.IMPORT_TASK_FAILED: { + case TasksActionTypes.DELETE_TASK_FAILED: { + const { taskID } = action.payload; return { ...state, errors: { ...state.errors, tasks: { ...state.errors.tasks, - importing: { - message: 'Could not import the task', + deleting: { + message: + 'Could not delete the ' + + `task ${taskID}`, reason: action.payload.error.toString(), + className: 'cvat-notification-notice-delete-task-failed', }, }, }, }; } - case TasksActionTypes.IMPORT_TASK_SUCCESS: { - const taskID = action.payload.task.id; + case TasksActionTypes.CREATE_TASK_FAILED: { return { ...state, - messages: { - ...state.messages, + errors: { + ...state.errors, tasks: { - ...state.messages.tasks, - importingDone: `Task has been imported succesfully Open task`, + ...state.errors.tasks, + creating: { + message: 'Could not create the task', + reason: action.payload.error.toString(), + className: 'cvat-notification-notice-create-task-failed', + }, }, }, }; @@ -621,51 +660,6 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case ProjectsActionTypes.BACKUP_PROJECT_FAILED: { - return { - ...state, - errors: { - ...state.errors, - projects: { - ...state.errors.projects, - backuping: { - message: `Could not backup the project #${action.payload.projectId}`, - reason: action.payload.error.toString(), - }, - }, - }, - }; - } - case ProjectsActionTypes.RESTORE_PROJECT_FAILED: { - return { - ...state, - errors: { - ...state.errors, - projects: { - ...state.errors.projects, - restoring: { - message: 'Could not restore the project', - reason: action.payload.error.toString(), - }, - }, - }, - }; - } - case ProjectsActionTypes.RESTORE_PROJECT_SUCCESS: { - const { projectID } = action.payload; - return { - ...state, - messages: { - ...state.messages, - projects: { - ...state.messages.projects, - restoringDone: - `Project has been created succesfully. - Click here to open`, - }, - }, - }; - } case FormatsActionTypes.GET_FORMATS_FAILED: { return { ...state, diff --git a/cvat-ui/src/reducers/projects-reducer.ts b/cvat-ui/src/reducers/projects-reducer.ts index c9a6f472..7f416b2d 100644 --- a/cvat-ui/src/reducers/projects-reducer.ts +++ b/cvat-ui/src/reducers/projects-reducer.ts @@ -1,9 +1,9 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { AnyAction } from 'redux'; -import { omit } from 'lodash'; import { ProjectsActionTypes } from 'actions/projects-actions'; import { BoundariesActionTypes } from 'actions/boundaries-actions'; import { AuthActionTypes } from 'actions/auth-actions'; @@ -37,9 +37,7 @@ const defaultState: ProjectsState = { id: null, error: '', }, - backups: {}, }, - restoring: false, }; export default (state: ProjectsState = defaultState, action: AnyAction): ProjectsState => { @@ -204,48 +202,6 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project }, }; } - case ProjectsActionTypes.BACKUP_PROJECT: { - const { projectId } = action.payload; - const { backups } = state.activities; - - return { - ...state, - activities: { - ...state.activities, - backups: { - ...backups, - ...Object.fromEntries([[projectId, true]]), - }, - }, - }; - } - case ProjectsActionTypes.BACKUP_PROJECT_FAILED: - case ProjectsActionTypes.BACKUP_PROJECT_SUCCESS: { - const { projectID } = action.payload; - const { backups } = state.activities; - - return { - ...state, - activities: { - ...state.activities, - backups: omit(backups, [projectID]), - }, - }; - } - case ProjectsActionTypes.RESTORE_PROJECT: { - return { - ...state, - restoring: true, - }; - } - case ProjectsActionTypes.RESTORE_PROJECT_FAILED: - case ProjectsActionTypes.RESTORE_PROJECT_SUCCESS: { - return { - ...state, - restoring: false, - }; - } - case BoundariesActionTypes.RESET_AFTER_ERROR: case AuthActionTypes.LOGOUT_SUCCESS: { return { ...defaultState }; diff --git a/cvat-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index 53b91afc..a55c4bc4 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-ui/src/reducers/tasks-reducer.ts b/cvat-ui/src/reducers/tasks-reducer.ts index 716b6468..43274646 100644 --- a/cvat-ui/src/reducers/tasks-reducer.ts +++ b/cvat-ui/src/reducers/tasks-reducer.ts @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -31,17 +32,14 @@ const defaultState: TasksState = { projectId: null, }, activities: { - loads: {}, deletes: {}, creates: { taskId: null, status: '', error: '', }, - backups: {}, jobUpdates: {}, }, - importing: false, }; export default (state: TasksState = defaultState, action: AnyAction): TasksState => { @@ -82,40 +80,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState initialized: true, fetching: false, }; - case TasksActionTypes.LOAD_ANNOTATIONS: { - const { task } = action.payload; - const { loader } = action.payload; - const { loads } = state.activities; - - loads[task.id] = task.id in loads ? loads[task.id] : loader.name; - - return { - ...state, - activities: { - ...state.activities, - loads: { - ...loads, - }, - }, - }; - } - case TasksActionTypes.LOAD_ANNOTATIONS_FAILED: - case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: { - const { task } = action.payload; - const { loads } = state.activities; - - delete loads[task.id]; - - return { - ...state, - activities: { - ...state.activities, - loads: { - ...loads, - }, - }, - }; - } case TasksActionTypes.DELETE_TASK: { const { taskID } = action.payload; const { deletes } = state.activities; @@ -164,49 +128,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState }, }; } - case TasksActionTypes.EXPORT_TASK: { - const { taskID } = action.payload; - const { backups } = state.activities; - - return { - ...state, - activities: { - ...state.activities, - backups: { - ...backups, - ...Object.fromEntries([[taskID, true]]), - }, - }, - }; - } - case TasksActionTypes.EXPORT_TASK_FAILED: - case TasksActionTypes.EXPORT_TASK_SUCCESS: { - const { taskID } = action.payload; - const { backups } = state.activities; - - delete backups[taskID]; - - return { - ...state, - activities: { - ...state.activities, - backups: omit(backups, [taskID]), - }, - }; - } - case TasksActionTypes.IMPORT_TASK: { - return { - ...state, - importing: true, - }; - } - case TasksActionTypes.IMPORT_TASK_FAILED: - case TasksActionTypes.IMPORT_TASK_SUCCESS: { - return { - ...state, - importing: false, - }; - } case TasksActionTypes.CREATE_TASK: { return { ...state, diff --git a/cvat/apps/engine/backup.py b/cvat/apps/engine/backup.py index 9441ad62..5cbcb327 100644 --- a/cvat/apps/engine/backup.py +++ b/cvat/apps/engine/backup.py @@ -1,4 +1,5 @@ # Copyright (C) 2021-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -31,7 +32,7 @@ from cvat.apps.engine.log import slogger from cvat.apps.engine.serializers import (AttributeSerializer, DataSerializer, LabeledDataSerializer, SegmentSerializer, SimpleJobSerializer, TaskReadSerializer, ProjectReadSerializer, ProjectFileSerializer, TaskFileSerializer) -from cvat.apps.engine.utils import av_scan_paths +from cvat.apps.engine.utils import av_scan_paths, process_failed_job, configure_dependent_job from cvat.apps.engine.models import ( StorageChoice, StorageMethodChoice, DataChoice, Task, Project, Location, CloudStorage as CloudStorageModel) @@ -39,7 +40,7 @@ from cvat.apps.engine.task import _create_thread 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.bindings import CvatImportError from cvat.apps.engine.cloud_provider import ( - db_storage_to_storage_instance, validate_bucket_status + db_storage_to_storage_instance, import_from_cloud_storage, export_to_cloud_storage ) from cvat.apps.engine.location import StorageType, get_location_configuration @@ -787,11 +788,6 @@ def export(db_instance, request): return sendfile(request, file_path, attachment=True, attachment_filename=filename) elif location == Location.CLOUD_STORAGE: - - @validate_bucket_status - def _export_to_cloud_storage(storage, file_path, file_name): - storage.upload_file(file_path, file_name) - try: storage_id = location_conf['storage_id'] except KeyError: @@ -801,7 +797,7 @@ def export(db_instance, request): db_storage = get_object_or_404(CloudStorageModel, pk=storage_id) storage = db_storage_to_storage_instance(db_storage) - _export_to_cloud_storage(storage, file_path, filename) + export_to_cloud_storage(storage, file_path, filename) return Response(status=status.HTTP_200_OK) else: raise NotImplementedError() @@ -825,6 +821,14 @@ def export(db_instance, request): result_ttl=ttl, failure_ttl=ttl) return Response(status=status.HTTP_202_ACCEPTED) + +def _download_file_from_bucket(db_storage, filename, key): + storage = db_storage_to_storage_instance(db_storage) + + data = import_from_cloud_storage(storage, key) + with open(filename, 'wb+') as f: + f.write(data.getbuffer()) + def _import(importer, request, rq_id, Serializer, file_field_name, location_conf, filename=None): queue = django_rq.get_queue("default") rq_job = queue.fetch_job(rq_id) @@ -832,6 +836,7 @@ def _import(importer, request, rq_id, Serializer, file_field_name, location_conf if not rq_job: org_id = getattr(request.iam_context['organization'], 'id', None) fd = None + dependent_job = None location = location_conf.get('location') if location == Location.LOCAL: @@ -844,14 +849,8 @@ def _import(importer, request, rq_id, Serializer, file_field_name, location_conf for chunk in payload_file.chunks(): f.write(chunk) else: - @validate_bucket_status - def _import_from_cloud_storage(storage, file_name): - return storage.download_fileobj(file_name) - file_name = request.query_params.get('filename') - assert file_name - - # download file from cloud storage + assert file_name, "The filename wasn't specified" try: storage_id = location_conf['storage_id'] except KeyError: @@ -859,13 +858,11 @@ def _import(importer, request, rq_id, Serializer, file_field_name, location_conf 'Cloud storage location was selected for destination' ' but cloud storage id was not specified') db_storage = get_object_or_404(CloudStorageModel, pk=storage_id) - storage = db_storage_to_storage_instance(db_storage) - - data = _import_from_cloud_storage(storage, file_name) - + key = filename fd, filename = mkstemp(prefix='cvat_', dir=settings.TMP_FILES_ROOT) - with open(filename, 'wb+') as f: - f.write(data.getbuffer()) + dependent_job = configure_dependent_job( + queue, rq_id, _download_file_from_bucket, + db_storage, filename, key) rq_job = queue.enqueue_call( func=importer, @@ -875,6 +872,7 @@ def _import(importer, request, rq_id, Serializer, file_field_name, location_conf 'tmp_file': filename, 'tmp_file_descriptor': fd, }, + depends_on=dependent_job ) else: if rq_job.is_finished: @@ -883,12 +881,9 @@ def _import(importer, request, rq_id, Serializer, file_field_name, location_conf os.remove(rq_job.meta['tmp_file']) rq_job.delete() return Response({'id': project_id}, status=status.HTTP_201_CREATED) - elif rq_job.is_failed: - if rq_job.meta['tmp_file_descriptor']: os.close(rq_job.meta['tmp_file_descriptor']) - os.remove(rq_job.meta['tmp_file']) - exc_info = str(rq_job.exc_info) - rq_job.delete() - + elif rq_job.is_failed or \ + rq_job.is_deferred and rq_job.dependency and rq_job.dependency.is_failed: + exc_info = process_failed_job(rq_job) # RQ adds a prefix with exception class name import_error_prefix = '{}.{}'.format( CvatImportError.__module__, CvatImportError.__name__) diff --git a/cvat/apps/engine/cloud_provider.py b/cvat/apps/engine/cloud_provider.py index eb43c657..7135225c 100644 --- a/cvat/apps/engine/cloud_provider.py +++ b/cvat/apps/engine/cloud_provider.py @@ -648,3 +648,11 @@ def db_storage_to_storage_instance(db_storage): 'specific_attributes': db_storage.get_specific_attributes() } return get_cloud_storage_instance(cloud_provider=db_storage.provider_type, **details) + +@validate_bucket_status +def import_from_cloud_storage(storage, file_name): + return storage.download_fileobj(file_name) + +@validate_bucket_status +def export_to_cloud_storage(storage, file_path, file_name): + storage.upload_file(file_path, file_name) diff --git a/cvat/apps/engine/mixins.py b/cvat/apps/engine/mixins.py index fa4e6153..d4ec577d 100644 --- a/cvat/apps/engine/mixins.py +++ b/cvat/apps/engine/mixins.py @@ -279,6 +279,11 @@ class AnnotationMixin: return Response(serializer.data) def import_annotations(self, request, pk, db_obj, import_func, rq_func, rq_id): + is_tus_request = request.headers.get('Upload-Length', None) is not None or \ + request.method == 'OPTIONS' + if is_tus_request: + return self.init_tus_upload(request) + use_default_location = request.query_params.get('use_default_location', True) use_settings = strtobool(str(use_default_location)) obj = db_obj if use_settings else request.query_params diff --git a/cvat/apps/engine/utils.py b/cvat/apps/engine/utils.py index a72bdfb9..ead30d65 100644 --- a/cvat/apps/engine/utils.py +++ b/cvat/apps/engine/utils.py @@ -108,3 +108,28 @@ def parse_specific_attributes(specific_attributes): return { key: value for (key, value) in parsed_specific_attributes } if parsed_specific_attributes else dict() + + +def process_failed_job(rq_job): + if rq_job.meta['tmp_file_descriptor']: + os.close(rq_job.meta['tmp_file_descriptor']) + if os.path.exists(rq_job.meta['tmp_file']): + os.remove(rq_job.meta['tmp_file']) + exc_info = str(rq_job.exc_info) or str(rq_job.dependency.exc_info) + if rq_job.dependency: + rq_job.dependency.delete() + rq_job.delete() + + return exc_info + +def configure_dependent_job(queue, rq_id, rq_func, db_storage, filename, key): + rq_job_id_download_file = rq_id + f'?action=download_{filename}' + rq_job_download_file = queue.fetch_job(rq_job_id_download_file) + if not rq_job_download_file: + # note: boto3 resource isn't pickleable, so we can't use storage + rq_job_download_file = queue.enqueue_call( + func=rq_func, + args=(db_storage, filename, key), + job_id=rq_job_id_download_file + ) + return rq_job_download_file diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 007e6f04..d2a10186 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -44,7 +44,9 @@ from django_sendfile import sendfile import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager.views # pylint: disable=unused-import from cvat.apps.engine.cloud_provider import ( - db_storage_to_storage_instance, validate_bucket_status, Status as CloudStorageStatus) + db_storage_to_storage_instance, import_from_cloud_storage, export_to_cloud_storage, + Status as CloudStorageStatus +) from cvat.apps.dataset_manager.bindings import CvatImportError from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer from cvat.apps.engine.frame_provider import FrameProvider @@ -67,9 +69,10 @@ from cvat.apps.engine.serializers import ( ProjectFileSerializer, TaskFileSerializer) from utils.dataset_manifest import ImageManifestManager -from cvat.apps.engine.utils import av_scan_paths +from cvat.apps.engine.utils import av_scan_paths, process_failed_job, configure_dependent_job from cvat.apps.engine import backup from cvat.apps.engine.mixins import PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin +from cvat.apps.engine.location import get_location_configuration, StorageType from . import models, task from .log import clogger, slogger @@ -392,14 +395,16 @@ class ProjectViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, elif rq_job.is_finished: if rq_job.meta['tmp_file_descriptor']: os.close(rq_job.meta['tmp_file_descriptor']) os.remove(rq_job.meta['tmp_file']) + if rq_job.dependency: + rq_job.dependency.delete() rq_job.delete() return Response(status=status.HTTP_201_CREATED) - elif rq_job.is_failed: - if rq_job.meta['tmp_file_descriptor']: os.close(rq_job.meta['tmp_file_descriptor']) - os.remove(rq_job.meta['tmp_file']) - rq_job.delete() + elif rq_job.is_failed or \ + rq_job.is_deferred and rq_job.dependency and rq_job.dependency.is_failed: + exc_info = process_failed_job(rq_job) + return Response( - data=str(rq_job.exc_info), + data=str(exc_info), status=status.HTTP_500_INTERNAL_SERVER_ERROR ) else: @@ -837,7 +842,6 @@ class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, # UploadMixin method def upload_finished(self, request): if self.action == 'annotations': - # db_task = self.get_object() format_name = request.query_params.get("format", "") filename = request.query_params.get("filename", "") tmp_dir = self._object.get_tmp_dirname() @@ -1073,12 +1077,18 @@ class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, elif request.method == 'PUT': format_name = request.query_params.get('format') if format_name: + use_settings = strtobool(str(request.query_params.get('use_default_location', True))) + obj = self._object if use_settings else request.query_params + location_conf = get_location_configuration( + obj=obj, use_settings=use_settings, field_name=StorageType.SOURCE + ) return _import_annotations( request=request, rq_id="{}@/api/tasks/{}/annotations/upload".format(request.user, pk), rq_func=dm.task.import_task_annotations, pk=pk, format_name=format_name, + location_conf=location_conf ) else: serializer = LabeledDataSerializer(data=request.data) @@ -1417,12 +1427,18 @@ class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, elif request.method == 'PUT': format_name = request.query_params.get('format', '') if format_name: + use_settings = strtobool(str(request.query_params.get('use_default_location', True))) + obj = self._object.segment.task if use_settings else request.query_params + location_conf = get_location_configuration( + obj=obj, use_settings=use_settings, field_name=StorageType.SOURCE + ) return _import_annotations( request=request, rq_id="{}@/api/jobs/{}/annotations/upload".format(request.user, pk), rq_func=dm.task.import_job_annotations, pk=pk, - format_name=format_name + format_name=format_name, + location_conf=location_conf ) else: serializer = LabeledDataSerializer(data=request.data) @@ -2115,13 +2131,12 @@ def rq_handler(job, exc_type, exc_value, tb): return True -@validate_bucket_status -def _export_to_cloud_storage(storage, file_path, file_name): - storage.upload_file(file_path, file_name) +def _download_file_from_bucket(db_storage, filename, key): + storage = db_storage_to_storage_instance(db_storage) -@validate_bucket_status -def _import_from_cloud_storage(storage, file_name): - return storage.download_fileobj(file_name) + data = import_from_cloud_storage(storage, key) + with open(filename, 'wb+') as f: + f.write(data.getbuffer()) def _import_annotations(request, rq_id, rq_func, pk, format_name, filename=None, location_conf=None): @@ -2141,6 +2156,7 @@ def _import_annotations(request, rq_id, rq_func, pk, format_name, # Then we dont need to create temporary file # Or filename specify key in cloud storage so we need to download file fd = None + dependent_job = None location = location_conf.get('location') if location_conf else Location.LOCAL if not filename or location == Location.CLOUD_STORAGE: @@ -2153,28 +2169,26 @@ def _import_annotations(request, rq_id, rq_func, pk, format_name, for chunk in anno_file.chunks(): f.write(chunk) else: - # download annotation file from cloud storage + assert filename, 'The filename was not spesified' try: storage_id = location_conf['storage_id'] except KeyError: - raise serializer.ValidationError( + raise serializers.ValidationError( 'Cloud storage location was selected for destination' ' but cloud storage id was not specified') db_storage = get_object_or_404(CloudStorageModel, pk=storage_id) - storage = db_storage_to_storage_instance(db_storage) - assert filename, 'filename was not spesified' - - data = _import_from_cloud_storage(storage, filename) - + key = filename fd, filename = mkstemp(prefix='cvat_{}'.format(pk), dir=settings.TMP_FILES_ROOT) - with open(filename, 'wb+') as f: - f.write(data.getbuffer()) + dependent_job = configure_dependent_job( + queue, rq_id, _download_file_from_bucket, + db_storage, filename, key) av_scan_paths(filename) rq_job = queue.enqueue_call( func=rq_func, args=(pk, filename, format_name), - job_id=rq_id + job_id=rq_id, + depends_on=dependent_job ) rq_job.meta['tmp_file'] = filename rq_job.meta['tmp_file_descriptor'] = fd @@ -2185,12 +2199,9 @@ def _import_annotations(request, rq_id, rq_func, pk, format_name, os.remove(rq_job.meta['tmp_file']) rq_job.delete() return Response(status=status.HTTP_201_CREATED) - elif rq_job.is_failed: - if rq_job.meta['tmp_file_descriptor']: os.close(rq_job.meta['tmp_file_descriptor']) - os.remove(rq_job.meta['tmp_file']) - exc_info = str(rq_job.exc_info) - rq_job.delete() - + elif rq_job.is_failed or \ + rq_job.is_deferred and rq_job.dependency and rq_job.dependency.is_failed: + exc_info = process_failed_job(rq_job) # RQ adds a prefix with exception class name import_error_prefix = '{}.{}'.format( CvatImportError.__module__, CvatImportError.__name__) @@ -2243,13 +2254,13 @@ def _export_annotations(db_instance, rq_id, request, format_name, action, callba db_instance.__class__.__name__.lower(), db_instance.name if isinstance(db_instance, (Task, Project)) else db_instance.id, timestamp, format_name, osp.splitext(file_path)[1] - ) + ).lower() # save annotation to specified location location = location_conf.get('location') if location == Location.LOCAL: return sendfile(request, file_path, attachment=True, - attachment_filename=filename.lower()) + attachment_filename=filename) elif location == Location.CLOUD_STORAGE: try: storage_id = location_conf['storage_id'] @@ -2261,7 +2272,7 @@ def _export_annotations(db_instance, rq_id, request, format_name, action, callba db_storage = get_object_or_404(CloudStorageModel, pk=storage_id) storage = db_storage_to_storage_instance(db_storage) - _export_to_cloud_storage(storage, file_path, filename) + export_to_cloud_storage(storage, file_path, filename) return Response(status=status.HTTP_200_OK) else: raise NotImplementedError() @@ -2309,6 +2320,7 @@ def _import_project_dataset(request, rq_id, rq_func, pk, format_name, filename=N if not rq_job: fd = None + dependent_job = None location = location_conf.get('location') if location_conf else None if not filename and location != Location.CLOUD_STORAGE: serializer = DatasetFileSerializer(data=request.data) @@ -2319,9 +2331,7 @@ def _import_project_dataset(request, rq_id, rq_func, pk, format_name, filename=N for chunk in dataset_file.chunks(): f.write(chunk) elif location == Location.CLOUD_STORAGE: - assert filename - - # download project file from cloud storage + assert filename, 'The filename was not spesified' try: storage_id = location_conf['storage_id'] except KeyError: @@ -2329,23 +2339,22 @@ def _import_project_dataset(request, rq_id, rq_func, pk, format_name, filename=N 'Cloud storage location was selected for destination' ' but cloud storage id was not specified') db_storage = get_object_or_404(CloudStorageModel, pk=storage_id) - storage = db_storage_to_storage_instance(db_storage) - - data = _import_from_cloud_storage(storage, filename) - - fd, filename = mkstemp(prefix='cvat_', dir=settings.TMP_FILES_ROOT) - with open(filename, 'wb+') as f: - f.write(data.getbuffer()) + key = filename + fd, filename = mkstemp(prefix='cvat_{}'.format(pk), dir=settings.TMP_FILES_ROOT) + dependent_job = configure_dependent_job( + queue, rq_id, _download_file_from_bucket, + db_storage, filename, key) rq_job = queue.enqueue_call( func=rq_func, args=(pk, filename, format_name), job_id=rq_id, meta={ - 'tmp_file': filename, - 'tmp_file_descriptor': fd, - }, - ) + 'tmp_file': filename, + 'tmp_file_descriptor': fd, + }, + depends_on=dependent_job + ) else: return Response(status=status.HTTP_409_CONFLICT, data='Import job already exists') diff --git a/tests/cypress/integration/actions_projects_models/case_103_project_export.js b/tests/cypress/integration/actions_projects_models/case_103_project_export.js index 04be7163..d29c5552 100644 --- a/tests/cypress/integration/actions_projects_models/case_103_project_export.js +++ b/tests/cypress/integration/actions_projects_models/case_103_project_export.js @@ -98,6 +98,7 @@ context('Export project dataset.', { browser: '!firefox' }, () => { datasetArchiveName = file; cy.verifyDownload(datasetArchiveName); }); + cy.verifyNotification(); }); it('Export project dataset. Dataset.', () => { @@ -143,6 +144,7 @@ context('Export project dataset.', { browser: '!firefox' }, () => { archive: datasetArchiveName, }; cy.importProject(importDataset); + cy.verifyNotification(); cy.openProject(projectName); cy.get('.cvat-tasks-list-item').should('have.length', 1); }); @@ -172,6 +174,7 @@ context('Export project dataset.', { browser: '!firefox' }, () => { archive: datasetArchiveName, }; cy.importProject(importDataset); + cy.verifyNotification(); cy.openProject(projectName); cy.get('.cvat-tasks-list-item').should('have.length', 1); cy.get('.cvat-constructor-viewer-item') diff --git a/tests/cypress/integration/actions_projects_models/case_104_project_export_3d.js b/tests/cypress/integration/actions_projects_models/case_104_project_export_3d.js index 9974caaa..1d41f6b3 100644 --- a/tests/cypress/integration/actions_projects_models/case_104_project_export_3d.js +++ b/tests/cypress/integration/actions_projects_models/case_104_project_export_3d.js @@ -76,6 +76,7 @@ context('Export project dataset with 3D task.', { browser: '!firefox' }, () => { datasetArchiveName = file; cy.verifyDownload(datasetArchiveName); }); + cy.verifyNotification(); }); it('Export project with 3D task. Annotation. Rename a archive.', () => { @@ -106,6 +107,7 @@ context('Export project dataset with 3D task.', { browser: '!firefox' }, () => { archive: datasetArchiveName, }; cy.importProject(importDataset); + cy.verifyNotification(); cy.openProject(projectName); cy.get('.cvat-tasks-list-item').should('have.length', 1); }); diff --git a/tests/cypress/integration/actions_projects_models/case_114_backup_restore_project.js b/tests/cypress/integration/actions_projects_models/case_114_backup_restore_project.js index eda5b2b3..9229fc7f 100644 --- a/tests/cypress/integration/actions_projects_models/case_114_backup_restore_project.js +++ b/tests/cypress/integration/actions_projects_models/case_114_backup_restore_project.js @@ -92,6 +92,7 @@ context('Backup, restore a project.', { browser: '!firefox' }, () => { projectBackupArchiveFullName = file; cy.verifyDownload(projectBackupArchiveFullName); }); + cy.verifyNotification(); }); it('Remove and restore the project from backup.', () => { @@ -156,6 +157,7 @@ context('Backup, restore a project with a 3D task.', { browser: '!firefox' }, () projectBackupArchiveFullName = file; cy.verifyDownload(projectBackupArchiveFullName); }); + cy.verifyNotification(); }); it('Remove and restore the project from backup.', () => { diff --git a/tests/cypress/integration/actions_tasks/case_52_dump_upload_annotation.js b/tests/cypress/integration/actions_tasks/case_52_dump_upload_annotation.js index d895dd4f..356039f2 100644 --- a/tests/cypress/integration/actions_tasks/case_52_dump_upload_annotation.js +++ b/tests/cypress/integration/actions_tasks/case_52_dump_upload_annotation.js @@ -43,18 +43,13 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { .parents('.cvat-tasks-list-item') .find('.cvat-menu-icon') .trigger('mouseover'); - cy.contains('Upload annotations').trigger('mouseover'); - cy.contains('.cvat-menu-load-submenu-item', exportFormat.split(' ')[0]) - .should('be.visible') - .within(() => { - cy.get('.cvat-menu-load-submenu-item-button').click(); - }); - // when a user clicks, menu is closing and it triggers rerender - // we use mouseout here to emulate user behaviour - cy.get('.cvat-actions-menu').trigger('mouseout').should('be.hidden'); - cy.contains('.cvat-menu-load-submenu-item', exportFormat.split(' ')[0]).within(() => { - cy.get('input[type=file]').attachFile(annotationArchiveNameCustomeName); - }); + cy.contains('Upload annotations').click(); + cy.get('.cvat-modal-import-dataset').find('.cvat-modal-import-select').click(); + cy.contains('.cvat-modal-import-dataset-option-item', exportFormat.split(' ')[0]).click(); + cy.get('.cvat-modal-import-select').should('contain.text', exportFormat.split(' ')[0]); + cy.get('input[type="file"]').attachFile(annotationArchiveNameCustomeName, { subjectType: 'drag-n-drop' }); + cy.get(`[title="${annotationArchiveNameCustomeName}"]`).should('be.visible'); + cy.contains('button', 'OK').click(); } function confirmUpdate(modalWindowClassName) { @@ -79,11 +74,12 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { format: exportFormat, archiveCustomeName: 'task_export_annotation_custome_name', }; - cy.exportTask(exportAnnotationRenameArchive); + cy.exportJob(exportAnnotationRenameArchive); cy.getDownloadFileName().then((file) => { annotationArchiveNameCustomeName = file; cy.verifyDownload(annotationArchiveNameCustomeName); }); + cy.verifyNotification(); }); it('Save job. Dump annotation. Remove annotation. Save job.', () => { @@ -92,11 +88,12 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { type: 'annotations', format: exportFormat, }; - cy.exportTask(exportAnnotation); + cy.exportJob(exportAnnotation); cy.getDownloadFileName().then((file) => { annotationArchiveName = file; cy.verifyDownload(annotationArchiveName); }); + cy.verifyNotification(); cy.removeAnnotations(); cy.saveJob('PUT'); cy.get('#cvat_canvas_shape_1').should('not.exist'); @@ -105,20 +102,19 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { it('Upload annotation to job.', () => { cy.interactMenu('Upload annotations'); - cy.contains('.cvat-menu-load-submenu-item', exportFormat.split(' ')[0]) - .should('be.visible') - .within(() => { - cy.get('.cvat-menu-load-submenu-item-button').click(); - }); - // when a user clicks, menu is closing and it triggers rerender - // we use mouseout here to emulate user behaviour - cy.get('.cvat-annotation-menu').trigger('mouseout').should('be.hidden'); - cy.contains('.cvat-menu-load-submenu-item', exportFormat.split(' ')[0]).within(() => { - cy.get('input[type=file]').attachFile(annotationArchiveName); - }); + cy.get('.cvat-modal-import-dataset'); + cy.get('.cvat-modal-import-select').click(); + cy.contains('.cvat-modal-import-dataset-option-item', exportFormat.split(' ')[0]).click(); + cy.get('.cvat-modal-import-select').should('contain.text', exportFormat.split(' ')[0]); + cy.get('input[type="file"]').attachFile(annotationArchiveName, { subjectType: 'drag-n-drop' }); + cy.get(`[title="${annotationArchiveName}"]`).should('be.visible'); + cy.contains('button', 'OK').click(); confirmUpdate('.cvat-modal-content-load-job-annotation'); cy.intercept('GET', '/api/jobs/**/annotations**').as('uploadAnnotationsGet'); + cy.get('.cvat-notification-notice-import-annotation-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-import-annotation-start'); cy.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200); + cy.verifyNotification(); cy.get('#cvat_canvas_shape_1').should('exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.removeAnnotations(); @@ -130,8 +126,9 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { cy.goToTaskList(); uploadToTask(taskName); confirmUpdate('.cvat-modal-content-load-task-annotation'); - cy.contains('Annotations have been loaded').should('be.visible'); - cy.get('[data-icon="close"]').click(); + cy.get('.cvat-notification-notice-import-annotation-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-import-annotation-start'); + cy.verifyNotification(); cy.openTaskJob(taskName, 0, false); cy.get('#cvat_canvas_shape_1').should('exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); @@ -154,6 +151,8 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { cy.createAnnotationTask(taskNameSecond, labelNameSecond, attrName, textDefaultValue, archiveName); uploadToTask(taskNameSecond); confirmUpdate('.cvat-modal-content-load-task-annotation'); + cy.get('.cvat-notification-notice-import-annotation-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-import-annotation-start'); cy.get('.cvat-notification-notice-load-annotation-failed') .should('exist') .find('[aria-label="close"]') diff --git a/tests/cypress/integration/actions_tasks/issue_2473_import_annotations_frames_dots_in_name.js b/tests/cypress/integration/actions_tasks/issue_2473_import_annotations_frames_dots_in_name.js index f04c125b..81f6c36f 100644 --- a/tests/cypress/integration/actions_tasks/issue_2473_import_annotations_frames_dots_in_name.js +++ b/tests/cypress/integration/actions_tasks/issue_2473_import_annotations_frames_dots_in_name.js @@ -36,11 +36,29 @@ context('Import annotations for frames with dots in name.', { browser: '!firefox let annotationArchiveName = ''; function confirmUpdate(modalWindowClassName) { - cy.get(modalWindowClassName).within(() => { + cy.get(modalWindowClassName).should('be.visible').within(() => { cy.contains('button', 'Update').click(); }); } + function uploadAnnotation(format, file, confirmModalClassName) { + cy.get('.cvat-modal-import-dataset').should('be.visible'); + cy.get('.cvat-modal-import-select').click(); + cy.get('.ant-select-dropdown') + .not('.ant-select-dropdown-hidden').within(() => { + cy.get('.rc-virtual-list-holder') + .contains('.cvat-modal-import-dataset-option-item', format) + .click(); + }); + cy.get('.cvat-modal-import-select').should('contain.text', format); + cy.get('input[type="file"]').attachFile(file, { subjectType: 'drag-n-drop' }); + cy.get(`[title="${file}"]`).should('be.visible'); + cy.contains('button', 'OK').click(); + confirmUpdate(confirmModalClassName); + cy.get('.cvat-notification-notice-import-annotation-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-import-annotation-start'); + } + before(() => { cy.visit('auth/login'); cy.login(); @@ -65,22 +83,21 @@ context('Import annotations for frames with dots in name.', { browser: '!firefox describe(`Testing case "${issueId}"`, () => { it('Save job. Dump annotation to YOLO format. Remove annotation. Save job.', () => { cy.saveJob('PATCH', 200, 'saveJobDump'); - cy.intercept('GET', '/api/tasks/**/annotations**').as('dumpAnnotations'); - cy.interactMenu('Export task dataset'); - cy.get('.cvat-modal-export-task').find('.cvat-modal-export-select').click(); + cy.intercept('GET', '/api/jobs/**/annotations**').as('dumpAnnotations'); + cy.interactMenu('Export job dataset'); + cy.get('.cvat-modal-export-select').click(); cy.get('.ant-select-dropdown') - .not('.ant-select-dropdown-hidden') - .within(() => { - cy.get('.rc-virtual-list-holder') - .contains('.cvat-modal-export-option-item', dumpType) - .scrollIntoView() - .should('be.visible') - .click(); - }); + .not('.ant-select-dropdown-hidden'); + cy.get('.rc-virtual-list-holder') + .contains('.cvat-modal-export-option-item', dumpType) + .click(); cy.get('.cvat-modal-export-select').should('contain.text', dumpType); - cy.get('.cvat-modal-export-task').contains('button', 'OK').click(); + cy.get('.cvat-modal-export-job').contains('button', 'OK').click(); + cy.get('.cvat-notification-notice-export-job-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-export-job-start'); cy.wait('@dumpAnnotations', { timeout: 5000 }).its('response.statusCode').should('equal', 202); cy.wait('@dumpAnnotations').its('response.statusCode').should('equal', 201); + cy.verifyNotification(); cy.removeAnnotations(); cy.saveJob('PUT'); cy.get('#cvat_canvas_shape_1').should('not.exist'); @@ -95,18 +112,15 @@ context('Import annotations for frames with dots in name.', { browser: '!firefox it('Upload annotation with YOLO format to job.', () => { cy.interactMenu('Upload annotations'); - cy.contains('.cvat-menu-load-submenu-item', dumpType.split(' ')[0]) - .scrollIntoView() - .should('be.visible') - .within(() => { - cy.get('.cvat-menu-load-submenu-item-button') - .click() - .get('input[type=file]') - .attachFile(annotationArchiveName); - }); + uploadAnnotation( + dumpType.split(' ')[0], + annotationArchiveName, + '.cvat-modal-content-load-job-annotation', + ); cy.intercept('GET', '/api/jobs/**/annotations?**').as('uploadAnnotationsGet'); - confirmUpdate('.cvat-modal-content-load-job-annotation'); cy.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200); + cy.contains('Annotations have been loaded').should('be.visible'); + cy.closeNotification('.ant-notification-notice-info'); cy.get('.cvat-notification-notice-upload-annotations-fail').should('not.exist'); cy.get('#cvat_canvas_shape_1').should('exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); diff --git a/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js b/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js index a1784b01..4b841b29 100644 --- a/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js +++ b/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js @@ -85,26 +85,38 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => { cy.get('.ant-dropdown') .not('.ant-dropdown-hidden') .within(() => { - cy.contains('[role="menuitem"]', new RegExp('^Backup Task$')).click().trigger('mouseout'); + cy.contains('[role="menuitem"]', new RegExp('^Backup Task$')).click(); }); + cy.get('.cvat-modal-export-task').find('.cvat-modal-export-filename-input').type(archiveName); + cy.get('.cvat-modal-export-task').contains('button', 'OK').click(); + cy.get('.cvat-notification-notice-export-backup-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-export-backup-start'); cy.getDownloadFileName().then((file) => { taskBackupArchiveFullName = file; cy.verifyDownload(taskBackupArchiveFullName); }); + cy.verifyNotification(); cy.deleteTask(taskName); }); it('Import the task. Check id, labels, shape.', () => { cy.intercept({ method: /PATCH|POST/, url: /\/api\/tasks\/backup.*/ }).as('importTask'); cy.get('.cvat-create-task-dropdown').click(); - cy.get('.cvat-import-task').click().find('input[type=file]').attachFile(taskBackupArchiveFullName); + cy.get('.cvat-import-task-button').click(); + cy.get('input[type=file]').attachFile(taskBackupArchiveFullName, { subjectType: 'drag-n-drop' }); + cy.get(`[title="${taskBackupArchiveFullName}"]`).should('be.visible'); + cy.contains('button', 'OK').click(); + cy.get('.cvat-notification-notice-import-backup-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-import-backup-start'); + cy.wait('@importTask').its('response.statusCode').should('equal', 202); cy.wait('@importTask').its('response.statusCode').should('equal', 201); cy.wait('@importTask').its('response.statusCode').should('equal', 204); cy.wait('@importTask').its('response.statusCode').should('equal', 202); cy.wait('@importTask', { timeout: 5000 }).its('response.statusCode').should('equal', 202); cy.wait('@importTask').its('response.statusCode').should('equal', 201); - cy.contains('Task has been imported succesfully').should('exist').and('be.visible'); + cy.contains('The task has been restored succesfully. Click here to open').should('exist').and('be.visible'); + cy.closeNotification('.ant-notification-notice-info'); cy.openTask(taskName); cy.url().then((link) => { expect(Number(link.split('/').slice(-1)[0])).to.be.equal(taskId + 1); diff --git a/tests/cypress/integration/actions_tasks3/case_47_export_dataset.js b/tests/cypress/integration/actions_tasks3/case_47_export_dataset.js index fe6eaae5..8454b903 100644 --- a/tests/cypress/integration/actions_tasks3/case_47_export_dataset.js +++ b/tests/cypress/integration/actions_tasks3/case_47_export_dataset.js @@ -26,24 +26,24 @@ context('Export task dataset.', () => { }); describe(`Testing case "${caseId}"`, () => { - it('Export a task as dataset.', () => { + it('Export a job as dataset.', () => { const exportDataset = { as: 'exportDataset', type: 'dataset', format: exportFormat, }; - cy.exportTask(exportDataset); + cy.exportJob(exportDataset); cy.waitForDownload(); }); - it('Export a task as dataset with renaming the archive.', () => { + it('Export a job as dataset with renaming the archive.', () => { const exportDataset = { as: 'exportDatasetRenameArchive', type: 'dataset', format: exportFormat, - archiveCustomeName: 'task_export_dataset_custome_name', + archiveCustomeName: 'job_export_dataset_custome_name', }; - cy.exportTask(exportDataset); + cy.exportJob(exportDataset); cy.waitForDownload(); }); }); diff --git a/tests/cypress/integration/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js b/tests/cypress/integration/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js index c0371417..5e70853f 100644 --- a/tests/cypress/integration/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js +++ b/tests/cypress/integration/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js @@ -21,9 +21,23 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format', }); } + function uploadAnnotation(format, file, confirmModalClassName) { + cy.get('.cvat-modal-import-dataset').should('be.visible'); + cy.get('.cvat-modal-import-select').click(); + cy.contains('.cvat-modal-import-dataset-option-item', format).click(); + cy.get('.cvat-modal-import-select').should('contain.text', format); + cy.get('input[type="file"]').attachFile(file, { subjectType: 'drag-n-drop' }); + cy.get(`[title="${file}"]`).should('be.visible'); + cy.contains('button', 'OK').click(); + confirmUpdate(confirmModalClassName); + cy.get('.cvat-notification-notice-import-annotation-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-import-annotation-start'); + } + before(() => { cy.openTask(taskName); cy.openJob(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000); // Waiting for the point cloud to display cy.create3DCuboid(cuboidCreationParams); cy.saveJob('PATCH', 200, 'saveJob'); @@ -36,11 +50,12 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format', type: 'annotations', format: dumpTypePC, }; - cy.exportTask(exportAnnotation); + cy.exportJob(exportAnnotation); cy.getDownloadFileName().then((file) => { annotationPCArchiveName = file; cy.verifyDownload(annotationPCArchiveName); }); + cy.verifyNotification(); }); it('Export with "Point Cloud" format. Renaming the archive', () => { @@ -48,35 +63,29 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format', as: 'exportAnnotationsRenameArchive', type: 'annotations', format: dumpTypePC, - archiveCustomeName: 'task_export_3d_annotation_custome_name_pc_format', + archiveCustomeName: 'job_export_3d_annotation_custome_name_pc_format', }; - cy.exportTask(exportAnnotationRenameArchive); + cy.exportJob(exportAnnotationRenameArchive); cy.getDownloadFileName().then((file) => { annotationPCArchiveCustomeName = file; cy.verifyDownload(annotationPCArchiveCustomeName); }); + cy.verifyNotification(); cy.removeAnnotations(); cy.saveJob('PUT'); cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); }); it('Upload "Point Cloud" format annotation to job.', () => { - cy.interactMenu('Upload annotations'); - cy.readFile(`cypress/fixtures/${annotationPCArchiveName}`, 'binary') - .then(Cypress.Blob.binaryStringToBlob) - .then((fileContent) => { - cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0]) - .should('be.visible') - .within(() => { - cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({ - fileContent, - fileName: annotationPCArchiveName, - }); - }); - }); - confirmUpdate('.cvat-modal-content-load-job-annotation'); cy.intercept('GET', '/api/jobs/**/annotations**').as('uploadAnnotationsGet'); + cy.interactMenu('Upload annotations'); + uploadAnnotation( + dumpTypePC.split(' ')[0], + annotationPCArchiveName, + '.cvat-modal-content-load-job-annotation', + ); cy.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200); + cy.verifyNotification(); cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.removeAnnotations(); cy.get('button').contains('Save').click().trigger('mouseout'); @@ -89,22 +98,14 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format', .parents('.cvat-tasks-list-item') .find('.cvat-menu-icon') .trigger('mouseover'); - cy.contains('Upload annotations').trigger('mouseover'); - cy.readFile(`cypress/fixtures/${annotationPCArchiveCustomeName}`, 'binary') - .then(Cypress.Blob.binaryStringToBlob) - .then((fileContent) => { - cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0]) - .should('be.visible') - .within(() => { - cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({ - fileName: annotationPCArchiveCustomeName, - fileContent, - }); - }); - }); - confirmUpdate('.cvat-modal-content-load-task-annotation'); + cy.contains('Upload annotations').click(); + uploadAnnotation( + dumpTypePC.split(' ')[0], + annotationPCArchiveName, + '.cvat-modal-content-load-task-annotation', + ); cy.contains('Annotations have been loaded').should('be.visible'); - cy.get('[data-icon="close"]').click(); + cy.closeNotification('.ant-notification-notice-info'); cy.openTaskJob(taskName); cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.removeAnnotations(); diff --git a/tests/cypress/integration/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js b/tests/cypress/integration/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js index ad67811d..0d6780dc 100644 --- a/tests/cypress/integration/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js +++ b/tests/cypress/integration/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js @@ -21,9 +21,23 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form }); } + function uploadAnnotation(format, file, confirmModalClassName) { + cy.get('.cvat-modal-import-dataset').should('be.visible'); + cy.get('.cvat-modal-import-select').click(); + cy.contains('.cvat-modal-import-dataset-option-item', format).click(); + cy.get('.cvat-modal-import-select').should('contain.text', format); + cy.get('input[type="file"]').attachFile(file, { subjectType: 'drag-n-drop' }); + cy.get(`[title="${file}"]`).should('be.visible'); + cy.contains('button', 'OK').click(); + confirmUpdate(confirmModalClassName); + cy.get('.cvat-notification-notice-import-annotation-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-import-annotation-start'); + } + before(() => { cy.openTask(taskName); cy.openJob(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000); // Waiting for the point cloud to display cy.create3DCuboid(cuboidCreationParams); cy.saveJob('PATCH', 200, 'saveJob'); @@ -36,11 +50,12 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form type: 'annotations', format: dumpTypeVC, }; - cy.exportTask(exportAnnotation); + cy.exportJob(exportAnnotation); cy.getDownloadFileName().then((file) => { annotationVCArchiveName = file; cy.verifyDownload(annotationVCArchiveName); }); + cy.verifyNotification(); }); it('Export with "Point Cloud" format. Renaming the archive', () => { @@ -48,35 +63,30 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form as: 'exportAnnotationsRenameArchive', type: 'annotations', format: dumpTypeVC, - archiveCustomeName: 'task_export_3d_annotation_custome_name_vc_format', + archiveCustomeName: 'job_export_3d_annotation_custome_name_vc_format', }; - cy.exportTask(exportAnnotationRenameArchive); + cy.exportJob(exportAnnotationRenameArchive); cy.getDownloadFileName().then((file) => { annotationVCArchiveNameCustomeName = file; cy.verifyDownload(annotationVCArchiveNameCustomeName); }); + cy.verifyNotification(); cy.removeAnnotations(); cy.saveJob('PUT'); cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); }); it('Upload "Velodyne Points" format annotation to job.', () => { - cy.interactMenu('Upload annotations'); - cy.readFile(`cypress/fixtures/${annotationVCArchiveName}`, 'binary') - .then(Cypress.Blob.binaryStringToBlob) - .then((fileContent) => { - cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0]) - .should('be.visible') - .within(() => { - cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({ - fileContent, - fileName: annotationVCArchiveName, - }); - }); - }); - confirmUpdate('.cvat-modal-content-load-job-annotation'); cy.intercept('GET', '/api/jobs/**/annotations**').as('uploadAnnotationsGet'); + cy.interactMenu('Upload annotations'); + uploadAnnotation( + dumpTypeVC.split(' ')[0], + annotationVCArchiveName, + '.cvat-modal-content-load-job-annotation', + ); cy.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200); + cy.contains('Annotations have been loaded').should('be.visible'); + cy.closeNotification('.ant-notification-notice-info'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.removeAnnotations(); cy.get('button').contains('Save').click().trigger('mouseout'); @@ -88,22 +98,14 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form .parents('.cvat-tasks-list-item') .find('.cvat-menu-icon') .trigger('mouseover'); - cy.contains('Upload annotations').trigger('mouseover'); - cy.readFile(`cypress/fixtures/${annotationVCArchiveNameCustomeName}`, 'binary') - .then(Cypress.Blob.binaryStringToBlob) - .then((fileContent) => { - cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0]) - .should('be.visible') - .within(() => { - cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({ - fileName: annotationVCArchiveNameCustomeName, - fileContent, - }); - }); - }); - confirmUpdate('.cvat-modal-content-load-task-annotation'); + cy.contains('Upload annotations').click(); + uploadAnnotation( + dumpTypeVC.split(' ')[0], + annotationVCArchiveNameCustomeName, + '.cvat-modal-content-load-task-annotation', + ); cy.contains('Annotations have been loaded').should('be.visible'); - cy.get('[data-icon="close"]').click(); + cy.closeNotification('.ant-notification-notice-info'); cy.openTaskJob(taskName); cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.removeAnnotations(); diff --git a/tests/cypress/integration/canvas3d_functionality/case_93_canvas3d_functionality_export_dataset.js b/tests/cypress/integration/canvas3d_functionality/case_93_canvas3d_functionality_export_dataset.js index 5b003fb7..a70f8172 100644 --- a/tests/cypress/integration/canvas3d_functionality/case_93_canvas3d_functionality_export_dataset.js +++ b/tests/cypress/integration/canvas3d_functionality/case_93_canvas3d_functionality_export_dataset.js @@ -18,6 +18,7 @@ context('Canvas 3D functionality. Export as a dataset.', () => { before(() => { cy.openTask(taskName); cy.openJob(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000); // Waiting for the point cloud to display cy.create3DCuboid(cuboidCreationParams); cy.saveJob(); @@ -30,7 +31,7 @@ context('Canvas 3D functionality. Export as a dataset.', () => { type: 'dataset', format: dumpTypePC, }; - cy.exportTask(exportDatasetPCFormat); + cy.exportJob(exportDatasetPCFormat); cy.waitForDownload(); }); @@ -40,7 +41,7 @@ context('Canvas 3D functionality. Export as a dataset.', () => { type: 'dataset', format: dumpTypeVC, }; - cy.exportTask(exportDatasetVCFormat); + cy.exportJob(exportDatasetVCFormat); cy.waitForDownload(); }); @@ -49,9 +50,9 @@ context('Canvas 3D functionality. Export as a dataset.', () => { as: 'exportDatasetVCFormatRenameArchive', type: 'dataset', format: dumpTypeVC, - archiveCustomeName: 'task_export_3d_dataset_custome_name_vc_format', + archiveCustomeName: 'job_export_3d_dataset_custome_name_vc_format', }; - cy.exportTask(exportDatasetVCFormatRenameArchive); + cy.exportJob(exportDatasetVCFormatRenameArchive); cy.waitForDownload(); cy.removeAnnotations(); cy.saveJob('PUT'); diff --git a/tests/cypress/integration/issues_prs2/issue_1568_cuboid_dump_annotation.js b/tests/cypress/integration/issues_prs2/issue_1568_cuboid_dump_annotation.js index 555a2649..3869ac98 100644 --- a/tests/cypress/integration/issues_prs2/issue_1568_cuboid_dump_annotation.js +++ b/tests/cypress/integration/issues_prs2/issue_1568_cuboid_dump_annotation.js @@ -35,12 +35,12 @@ context('Dump annotation if cuboid created.', () => { type: 'annotations', format: exportFormat, }; - cy.exportTask(exportAnnotation); + cy.exportJob(exportAnnotation); cy.waitForDownload(); }); it('Error notification is not exists.', () => { - cy.get('.ant-notification-notice').should('not.exist'); + cy.get('.ant-notification-notice-error').should('not.exist'); }); }); }); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index be36eb28..bba80eba 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -864,7 +864,7 @@ Cypress.Commands.add('exportTask', ({ cy.contains('.cvat-modal-export-option-item', format).should('be.visible').click(); cy.get('.cvat-modal-export-task').find('.cvat-modal-export-select').should('contain.text', format); if (type === 'dataset') { - cy.get('.cvat-modal-export-task').find('[type="checkbox"]').should('not.be.checked').check(); + cy.get('.cvat-modal-export-task').find('.cvat-modal-export-save-images').should('not.be.checked').click(); } if (archiveCustomeName) { cy.get('.cvat-modal-export-task').find('.cvat-modal-export-filename-input').type(archiveCustomeName); @@ -874,6 +874,24 @@ Cypress.Commands.add('exportTask', ({ cy.closeNotification('.cvat-notification-notice-export-task-start'); }); +Cypress.Commands.add('exportJob', ({ + type, format, archiveCustomeName, +}) => { + cy.interactMenu('Export job dataset'); + cy.get('.cvat-modal-export-job').should('be.visible').find('.cvat-modal-export-select').click(); + cy.contains('.cvat-modal-export-option-item', format).should('be.visible').click(); + cy.get('.cvat-modal-export-job').find('.cvat-modal-export-select').should('contain.text', format); + if (type === 'dataset') { + cy.get('.cvat-modal-export-job').find('.cvat-modal-export-save-images').should('not.be.checked').click(); + } + if (archiveCustomeName) { + cy.get('.cvat-modal-export-job').find('.cvat-modal-export-filename-input').type(archiveCustomeName); + } + cy.contains('button', 'OK').click(); + cy.get('.cvat-notification-notice-export-job-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-export-job-start'); +}); + Cypress.Commands.add('renameTask', (oldName, newName) => { cy.get('.cvat-task-details-task-name').within(() => { cy.get('[aria-label="edit"]').click(); @@ -928,6 +946,11 @@ Cypress.Commands.add('deleteFrame', (action = 'delete') => { cy.wait('@patchMeta').its('response.statusCode').should('equal', 200); }); +Cypress.Commands.add('verifyNotification', () => { + cy.get('.ant-notification-notice-info').should('be.visible'); + cy.closeNotification('.ant-notification-notice-info'); +}); + Cypress.Commands.overwrite('visit', (orig, url, options) => { orig(url, options); cy.closeModalUnsupportedPlatform(); diff --git a/tests/cypress/support/commands_projects.js b/tests/cypress/support/commands_projects.js index dea1042e..21efb8b4 100644 --- a/tests/cypress/support/commands_projects.js +++ b/tests/cypress/support/commands_projects.js @@ -104,13 +104,14 @@ Cypress.Commands.add('exportProject', ({ cy.contains('.cvat-modal-export-option-item', dumpType).should('be.visible').click(); cy.get('.cvat-modal-export-select').should('contain.text', dumpType); if (type === 'dataset') { - cy.get('.cvat-modal-export-project').find('[type="checkbox"]').should('not.be.checked').check(); + cy.get('.cvat-modal-export-project').find('.cvat-modal-export-save-images').should('not.be.checked').click(); } if (archiveCustomeName) { cy.get('.cvat-modal-export-project').find('.cvat-modal-export-filename-input').type(archiveCustomeName); } cy.get('.cvat-modal-export-project').contains('button', 'OK').click(); cy.get('.cvat-notification-notice-export-project-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-export-project-start'); }); Cypress.Commands.add('importProject', ({ @@ -131,28 +132,42 @@ Cypress.Commands.add('importProject', ({ cy.contains('button', 'OK').click(); cy.get('.cvat-modal-import-dataset-status').should('be.visible'); cy.get('.cvat-notification-notice-import-dataset-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-import-dataset-start'); cy.get('.cvat-modal-import-dataset-status').should('not.exist'); }); -Cypress.Commands.add('backupProject', (projectName) => { +Cypress.Commands.add('backupProject', (projectName, backupFileName) => { cy.projectActions(projectName); cy.get('.cvat-project-actions-menu').contains('Backup Project').click(); + cy.get('.cvat-modal-export-project').should('be.visible'); + if (backupFileName) { + cy.get('.cvat-modal-export-project').find('.cvat-modal-export-filename-input').type(backupFileName); + } + cy.get('.cvat-modal-export-project').contains('button', 'OK').click(); + cy.get('.cvat-notification-notice-export-backup-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-export-backup-start'); }); Cypress.Commands.add('restoreProject', (archiveWithBackup) => { cy.intercept({ method: /PATCH|POST/, url: /\/api\/projects\/backup.*/ }).as('restoreProject'); cy.get('.cvat-create-project-dropdown').click(); - cy.get('.cvat-import-project').click().find('input[type=file]').attachFile(archiveWithBackup); + cy.get('.cvat-import-project-button').click(); + cy.get('input[type=file]').attachFile(archiveWithBackup, { subjectType: 'drag-n-drop' }); + cy.get(`[title="${archiveWithBackup}"]`).should('be.visible'); + cy.contains('button', 'OK').click(); + cy.get('.cvat-notification-notice-import-backup-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-import-backup-start'); + cy.wait('@restoreProject').its('response.statusCode').should('equal', 202); cy.wait('@restoreProject').its('response.statusCode').should('equal', 201); cy.wait('@restoreProject').its('response.statusCode').should('equal', 204); cy.wait('@restoreProject').its('response.statusCode').should('equal', 202); cy.wait('@restoreProject', { timeout: 5000 }).its('response.statusCode').should('equal', 202); cy.wait('@restoreProject').its('response.statusCode').should('equal', 201); - cy.contains('Project has been created succesfully') + cy.contains('The project has been restored succesfully. Click here to open') .should('exist') .and('be.visible'); - cy.get('[data-icon="close"]').click(); // Close the notification + cy.closeNotification('.ant-notification-notice-info'); }); Cypress.Commands.add('getDownloadFileName', () => { @@ -168,6 +183,7 @@ Cypress.Commands.add('waitForDownload', () => { cy.getDownloadFileName().then((filename) => { cy.verifyDownload(filename); }); + cy.verifyNotification(); }); Cypress.Commands.add('deleteProjectViaActions', (projectName) => {