Source & target storage support (#4842)

main
Maria Khrustaleva 3 years ago committed by GitHub
parent a50d38f9e9
commit 9f89787f95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -49,6 +49,9 @@ Skeleton (<https://github.com/cvat-ai/cvat/pull/1>), (<https://github.com/opencv
<https://github.com/opencv/cvat/pull/4908>) <https://github.com/opencv/cvat/pull/4908>)
- Support for Oracle OCI Buckets (<https://github.com/opencv/cvat/pull/4876>) - Support for Oracle OCI Buckets (<https://github.com/opencv/cvat/pull/4876>)
- `cvat-sdk` and `cvat-cli` packages on PyPI (<https://github.com/opencv/cvat/pull/4903>) - `cvat-sdk` and `cvat-cli` packages on PyPI (<https://github.com/opencv/cvat/pull/4903>)
- UI part for source and target storages (<https://github.com/opencv/cvat/pull/4842>)
- Backup import/export modals (<https://github.com/opencv/cvat/pull/4842>)
- Annotations import modal (<https://github.com/opencv/cvat/pull/4842>)
### Changed ### Changed
- Bumped nuclio version to 1.8.14 - Bumped nuclio version to 1.8.14

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "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", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts", "main": "src/api.ts",
"scripts": { "scripts": {

@ -4,7 +4,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
(() => { (() => {
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
const { Task } = require('./session'); const { Task } = require('./session');
const { ScriptingError } = require('./exceptions'); const { ScriptingError } = require('./exceptions');

@ -1,404 +1,387 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
(() => { import { Storage } from './storage';
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
const Collection = require('./annotations-collection'); const Collection = require('./annotations-collection');
const AnnotationsSaver = require('./annotations-saver'); const AnnotationsSaver = require('./annotations-saver');
const AnnotationsHistory = require('./annotations-history').default; const AnnotationsHistory = require('./annotations-history').default;
const { checkObjectType } = require('./common'); const { checkObjectType } = require('./common');
const { Project } = require('./project'); const Project = require('./project').default;
const { Task, Job } = require('./session'); const { Task, Job } = require('./session');
const { Loader } = require('./annotation-formats'); const { ScriptingError, DataError, ArgumentError } = require('./exceptions');
const { ScriptingError, DataError, ArgumentError } = require('./exceptions'); const { getDeletedFrames } = require('./frames');
const { getDeletedFrames } = require('./frames');
const jobCache = new WeakMap();
const jobCache = new WeakMap(); const taskCache = new WeakMap();
const taskCache = new WeakMap();
function getCache(sessionType) {
function getCache(sessionType) { if (sessionType === 'task') {
if (sessionType === 'task') { return taskCache;
return taskCache; }
}
if (sessionType === 'job') { if (sessionType === 'job') {
return jobCache; return jobCache;
} }
throw new ScriptingError(`Unknown session type was received ${sessionType}`); throw new ScriptingError(`Unknown session type was received ${sessionType}`);
} }
async function getAnnotationsFromServer(session) { async function getAnnotationsFromServer(session) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
if (!cache.has(session)) { if (!cache.has(session)) {
const rawAnnotations = await serverProxy.annotations.getAnnotations(sessionType, session.id); const rawAnnotations = await serverProxy.annotations.getAnnotations(sessionType, session.id);
// Get meta information about frames // Get meta information about frames
const startFrame = sessionType === 'job' ? session.startFrame : 0; const startFrame = sessionType === 'job' ? session.startFrame : 0;
const stopFrame = sessionType === 'job' ? session.stopFrame : session.size - 1; const stopFrame = sessionType === 'job' ? session.stopFrame : session.size - 1;
const frameMeta = {}; const frameMeta = {};
for (let i = startFrame; i <= stopFrame; i++) { for (let i = startFrame; i <= stopFrame; i++) {
frameMeta[i] = await session.frames.get(i); frameMeta[i] = await session.frames.get(i);
} }
frameMeta.deleted_frames = await getDeletedFrames(sessionType, session.id); frameMeta.deleted_frames = await getDeletedFrames(sessionType, session.id);
const history = new AnnotationsHistory(); const history = new AnnotationsHistory();
const collection = new Collection({ const collection = new Collection({
labels: session.labels || session.task.labels, labels: session.labels || session.task.labels,
history, history,
startFrame, startFrame,
stopFrame, stopFrame,
frameMeta, frameMeta,
}); });
// eslint-disable-next-line no-unsanitized/method // eslint-disable-next-line no-unsanitized/method
collection.import(rawAnnotations); collection.import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
cache.set(session, { collection, saver, history }); cache.set(session, { collection, saver, history });
}
} }
}
async function closeSession(session) { export async function closeSession(session) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
if (cache.has(session)) { if (cache.has(session)) {
cache.delete(session); cache.delete(session);
}
} }
}
async function getAnnotations(session, frame, allTracks, filters) { export async function getAnnotations(session, frame, allTracks, filters) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
if (cache.has(session)) { if (cache.has(session)) {
return cache.get(session).collection.get(frame, allTracks, filters);
}
await getAnnotationsFromServer(session);
return cache.get(session).collection.get(frame, allTracks, filters); return cache.get(session).collection.get(frame, allTracks, filters);
} }
async function saveAnnotations(session, onUpdate) { await getAnnotationsFromServer(session);
const sessionType = session instanceof Task ? 'task' : 'job'; return cache.get(session).collection.get(frame, allTracks, filters);
const cache = getCache(sessionType); }
if (cache.has(session)) { export async function saveAnnotations(session, onUpdate) {
await cache.get(session).saver.save(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) { // If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it
const sessionType = session instanceof Task ? 'task' : 'job'; }
const cache = getCache(sessionType);
if (cache.has(session)) { export function searchAnnotations(session, filters, frameFrom, frameTo) {
return cache.get(session).collection.search(filters, frameFrom, frameTo); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).collection.search(filters, frameFrom, frameTo);
);
} }
function searchEmptyFrame(session, frameFrom, frameTo) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function searchEmptyFrame(session, frameFrom, frameTo) {
return cache.get(session).collection.searchEmpty(frameFrom, frameTo); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).collection.searchEmpty(frameFrom, frameTo);
);
} }
function mergeAnnotations(session, objectStates) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function mergeAnnotations(session, objectStates) {
return cache.get(session).collection.merge(objectStates); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).collection.merge(objectStates);
);
} }
function splitAnnotations(session, objectState, frame) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function splitAnnotations(session, objectState, frame) {
return cache.get(session).collection.split(objectState, frame); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).collection.split(objectState, frame);
);
} }
function groupAnnotations(session, objectStates, reset) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function groupAnnotations(session, objectStates, reset) {
return cache.get(session).collection.group(objectStates, reset); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).collection.group(objectStates, reset);
);
} }
function hasUnsavedChanges(session) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function hasUnsavedChanges(session) {
return cache.get(session).saver.hasUnsavedChanges(); 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) { return false;
checkObjectType('reload', reload, 'boolean', null); }
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) { export async function clearAnnotations(session, reload, startframe, endframe, delTrackKeyframesOnly) {
cache.get(session).collection.clear(startframe, endframe, delTrackKeyframesOnly); checkObjectType('reload', reload, 'boolean', null);
} const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (reload) { if (cache.has(session)) {
cache.delete(session); cache.get(session).collection.clear(startframe, endframe, delTrackKeyframesOnly);
await getAnnotationsFromServer(session);
}
} }
function annotationsStatistics(session) { if (reload) {
const sessionType = session instanceof Task ? 'task' : 'job'; cache.delete(session);
const cache = getCache(sessionType); await getAnnotationsFromServer(session);
}
}
if (cache.has(session)) { export function annotationsStatistics(session) {
return cache.get(session).collection.statistics(); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).collection.statistics();
);
} }
function putAnnotations(session, objectStates) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function putAnnotations(session, objectStates) {
return cache.get(session).collection.put(objectStates); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).collection.put(objectStates);
);
} }
function selectObject(session, objectStates, x, y) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function selectObject(session, objectStates, x, y) {
return cache.get(session).collection.select(objectStates, x, y); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).collection.select(objectStates, x, y);
);
} }
async function uploadAnnotations(session, file, loader) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
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);
}
function importAnnotations(session, data) { export function importCollection(session, data) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
if (cache.has(session)) { if (cache.has(session)) {
// eslint-disable-next-line no-unsanitized/method // eslint-disable-next-line no-unsanitized/method
return cache.get(session).collection.import(data); return cache.get(session).collection.import(data);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
} }
function exportAnnotations(session) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function exportCollection(session) {
return cache.get(session).collection.export(); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).collection.export();
);
} }
async function exportDataset(instance, format, name, saveImages = false) { throw new DataError(
if (!(format instanceof String || typeof format === 'string')) { 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
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'); export async function exportDataset(
} instance,
if (typeof saveImages !== 'boolean') { format: string,
throw new ArgumentError('Save images parameter must be a boolean'); 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; let result = null;
if (instance instanceof Task) { if (instance instanceof Task) {
result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages); result = await serverProxy.tasks
} else if (instance instanceof Job) { .exportDataset(instance.id, format, saveImages, useDefaultSettings, targetStorage, name);
result = await serverProxy.tasks.exportDataset(instance.taskId, format, name, saveImages); } else if (instance instanceof Job) {
} else { result = await serverProxy.jobs
result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages); .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 (instance instanceof Project) {
if (!(typeof format === 'string')) { return serverProxy.projects
throw new ArgumentError('Format must be a string'); .importDataset(instance.id, format, useDefaultSettings, sourceStorage, file, updateStatusCallback);
}
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);
} }
function getHistory(session) { const instanceType = instance instanceof Task ? 'task' : 'job';
const sessionType = session instanceof Task ? 'task' : 'job'; return serverProxy.annotations
const cache = getCache(sessionType); .uploadAnnotations(instanceType, instance.id, format, useDefaultSettings, sourceStorage, file);
}
if (cache.has(session)) { export function getHistory(session) {
return cache.get(session).history; const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).history;
);
} }
async function undoActions(session, count) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export async function undoActions(session, count) {
return cache.get(session).history.undo(count); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).history.undo(count);
);
} }
async function redoActions(session, count) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export async function redoActions(session, count) {
return cache.get(session).history.redo(count); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).history.redo(count);
);
} }
function freezeHistory(session, frozen) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function freezeHistory(session, frozen) {
return cache.get(session).history.freeze(frozen); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).history.freeze(frozen);
);
} }
function clearActions(session) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function clearActions(session) {
return cache.get(session).history.clear(); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
throw new DataError( if (cache.has(session)) {
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', return cache.get(session).history.clear();
);
} }
function getActions(session) { throw new DataError(
const sessionType = session instanceof Task ? 'task' : 'job'; 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
const cache = getCache(sessionType); );
}
if (cache.has(session)) { export function getActions(session) {
return cache.get(session).history.get(); const sessionType = session instanceof Task ? 'task' : 'job';
} const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.get();
}
throw new DataError( throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', '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,
};
})();

@ -6,7 +6,7 @@ const config = require('./config');
(() => { (() => {
const PluginRegistry = require('./plugins').default; const PluginRegistry = require('./plugins').default;
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
const lambdaManager = require('./lambda-manager'); const lambdaManager = require('./lambda-manager');
const { const {
isBoolean, isBoolean,
@ -21,7 +21,7 @@ const config = require('./config');
const { AnnotationFormats } = require('./annotation-formats'); const { AnnotationFormats } = require('./annotation-formats');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task, Job } = require('./session'); const { Task, Job } = require('./session');
const { Project } = require('./project'); const Project = require('./project').default;
const { CloudStorage } = require('./cloud-storage'); const { CloudStorage } = require('./cloud-storage');
const Organization = require('./organization'); const Organization = require('./organization');

@ -16,8 +16,8 @@ function build() {
const Comment = require('./comment'); const Comment = require('./comment');
const Issue = require('./issue'); const Issue = require('./issue');
const { Job, Task } = require('./session'); const { Job, Task } = require('./session');
const { Project } = require('./project'); const Project = require('./project').default;
const implementProject = require('./project-implementation'); const implementProject = require('./project-implementation').default;
const { Attribute, Label } = require('./labels'); const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model'); const MLModel = require('./ml-model');
const { FrameData } = require('./frames'); const { FrameData } = require('./frames');

@ -4,7 +4,7 @@
(() => { (() => {
const PluginRegistry = require('./plugins').default; const PluginRegistry = require('./plugins').default;
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
const { isBrowser, isNode } = require('browser-or-node'); const { isBrowser, isNode } = require('browser-or-node');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { CloudStorageCredentialsType, CloudStorageProviderType } = require('./enums'); const { CloudStorageCredentialsType, CloudStorageProviderType } = require('./enums');

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier = MIT // SPDX-License-Identifier = MIT
@ -389,7 +390,7 @@ export enum CloudStorageCredentialsType {
} }
/** /**
* Task statuses * Membership roles
* @enum {string} * @enum {string}
* @name MembershipRole * @name MembershipRole
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
@ -423,3 +424,17 @@ export enum SortingMethod {
PREDEFINED = 'predefined', PREDEFINED = 'predefined',
RANDOM = 'random', 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',
}

@ -171,7 +171,7 @@ export class Exception extends Error {
}; };
try { try {
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
await serverProxy.server.exception(exceptionObject); await serverProxy.server.exception(exceptionObject);
} catch (exception) { } catch (exception) {
// add event // add event

@ -5,7 +5,7 @@
(() => { (() => {
const cvatData = require('cvat-data'); const cvatData = require('cvat-data');
const PluginRegistry = require('./plugins').default; const PluginRegistry = require('./plugins').default;
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
const { isBrowser, isNode } = require('browser-or-node'); const { isBrowser, isNode } = require('browser-or-node');
const { Exception, ArgumentError, DataError } = require('./exceptions'); const { Exception, ArgumentError, DataError } = require('./exceptions');

@ -8,7 +8,7 @@ const PluginRegistry = require('./plugins').default;
const Comment = require('./comment'); const Comment = require('./comment');
const User = require('./user'); const User = require('./user');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
/** /**
* Class representing a single issue * Class representing a single issue

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const MLModel = require('./ml-model'); const MLModel = require('./ml-model');
const { RQStatus } = require('./enums'); const { RQStatus } = require('./enums');

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const PluginRegistry = require('./plugins').default; const PluginRegistry = require('./plugins').default;
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
const logFactory = require('./log'); const logFactory = require('./log');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { LogType } = require('./enums'); const { LogType } = require('./enums');

@ -7,7 +7,7 @@ const config = require('./config');
const { MembershipRole } = require('./enums'); const { MembershipRole } = require('./enums');
const { ArgumentError, ServerError } = require('./exceptions'); const { ArgumentError, ServerError } = require('./exceptions');
const PluginRegistry = require('./plugins').default; const PluginRegistry = require('./plugins').default;
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy').default;
const User = require('./user'); const User = require('./user');
/** /**

@ -1,93 +1,108 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
(() => { import { Storage } from './storage';
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;
}
// initial creating const serverProxy = require('./server-proxy').default;
const projectSpec = { const { getPreview } = require('./frames');
name: this.name,
labels: this.labels.map((el) => el.toJSON()),
};
if (this.bugTracker) { const Project = require('./project').default;
projectSpec.bug_tracker = this.bugTracker; const { exportDataset, importDataset } = require('./annotations');
}
if (this.trainingProject) { export default function implementProject(projectClass) {
projectSpec.training_project = this.trainingProject; 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); await serverProxy.projects.save(this.id, projectData);
return new Project(project); this._updateTrigger.reset();
}; return this;
}
projectClass.prototype.delete.implementation = async function () { // initial creating
const result = await serverProxy.projects.delete(this.id); const projectSpec: any = {
return result; name: this.name,
labels: this.labels.map((el) => el.toJSON()),
}; };
projectClass.prototype.preview.implementation = async function () { if (this.bugTracker) {
if (!this._internalData.task_ids.length) { projectSpec.bug_tracker = this.bugTracker;
return ''; }
}
const frameData = await getPreview(this._internalData.task_ids[0]);
return frameData;
};
projectClass.prototype.annotations.exportDataset.implementation = async function ( if (this.trainingProject) {
format, projectSpec.training_project = this.trainingProject;
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);
};
projectClass.prototype.backup.implementation = async function () { if (this.targetStorage) {
const result = await serverProxy.projects.backupProject(this.id); projectSpec.target_storage = this.targetStorage.toJSON();
return result; }
};
projectClass.restore.implementation = async function (file) { if (this.sourceStorage) {
const result = await serverProxy.projects.restoreProject(file); projectSpec.source_storage = this.sourceStorage.toJSON();
return result.id; }
};
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;
})(); }

@ -1,374 +1,429 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
(() => { import { StorageLocation } from './enums';
const PluginRegistry = require('./plugins').default; import { Storage } from './storage';
const { ArgumentError } = require('./exceptions');
const { Label } = require('./labels');
const User = require('./user');
const { FieldUpdateTrigger } = require('./common');
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 * In a fact you need use the constructor only if you want to create a project
* @memberof module:API.cvat.classes * @param {object} initialData - Object which is used for initialization
* <br> It can contain keys:
* <br> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> labels
*/ */
class Project { constructor(initialData) {
/** const data = {
* In a fact you need use the constructor only if you want to create a project id: undefined,
* @param {object} initialData - Object which is used for initialization name: undefined,
* <br> It can contain keys: status: undefined,
* <br> <li style="margin-left: 10px;"> name assignee: undefined,
* <br> <li style="margin-left: 10px;"> labels owner: undefined,
*/ bug_tracker: undefined,
constructor(initialData) { created_date: undefined,
const data = { updated_date: undefined,
id: undefined, task_subsets: undefined,
name: undefined, training_project: undefined,
status: undefined, task_ids: undefined,
assignee: undefined, dimension: undefined,
owner: undefined, source_storage: undefined,
bug_tracker: undefined, target_storage: undefined,
created_date: undefined, labels: undefined,
updated_date: undefined, };
task_subsets: undefined,
training_project: undefined,
task_ids: undefined,
dimension: undefined,
};
const updateTrigger = new FieldUpdateTrigger(); const updateTrigger = new FieldUpdateTrigger();
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property]; data[property] = initialData[property];
}
} }
}
data.labels = []; data.labels = [];
if (Array.isArray(initialData.labels)) { if (Array.isArray(initialData.labels)) {
data.labels = initialData.labels data.labels = initialData.labels
.map((labelData) => new Label(labelData)).filter((label) => !label.hasParent); .map((labelData) => new Label(labelData)).filter((label) => !label.hasParent);
} }
if (typeof initialData.training_project === 'object') { if (typeof initialData.training_project === 'object') {
data.training_project = { ...initialData.training_project }; data.training_project = { ...initialData.training_project };
} }
Object.defineProperties( Object.defineProperties(
this, this,
Object.freeze({ Object.freeze({
/** /**
* @name id * @name id
* @type {number} * @type {number}
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @readonly * @readonly
* @instance * @instance
*/ */
id: { id: {
get: () => data.id, get: () => data.id,
}, },
/** /**
* @name name * @name name
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @instance * @instance
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
name: { name: {
get: () => data.name, get: () => data.name,
set: (value) => { set: (value) => {
if (!value.trim().length) { if (!value.trim().length) {
throw new ArgumentError('Value must not be empty'); throw new ArgumentError('Value must not be empty');
} }
data.name = value; data.name = value;
updateTrigger.update('name'); updateTrigger.update('name');
},
}, },
},
/** /**
* @name status * @name status
* @type {module:API.cvat.enums.TaskStatus} * @type {module:API.cvat.enums.TaskStatus}
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @readonly * @readonly
* @instance * @instance
*/ */
status: { status: {
get: () => data.status, get: () => data.status,
}, },
/** /**
* Instance of a user who was assigned for the project * Instance of a user who was assigned for the project
* @name assignee * @name assignee
* @type {module:API.cvat.classes.User} * @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @readonly * @readonly
* @instance * @instance
*/ */
assignee: { assignee: {
get: () => data.assignee, get: () => data.assignee,
set: (assignee) => { set: (assignee) => {
if (assignee !== null && !(assignee instanceof User)) { if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance'); throw new ArgumentError('Value must be a user instance');
} }
data.assignee = assignee; data.assignee = assignee;
updateTrigger.update('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,
}, },
/** },
* Dimesion of the tasks in the project, if no task dimension is null /**
* @name dimension * Instance of a user who has created the project
* @type {string} * @name owner
* @memberof module:API.cvat.classes.Project * @type {module:API.cvat.classes.User}
* @readonly * @memberof module:API.cvat.classes.Project
* @instance * @readonly
*/ * @instance
dimension: { */
get: () => data.dimension, 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 * @name createdDate
* @type {module:API.cvat.classes.Label[]} * @type {string}
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @instance * @readonly
* @throws {module:API.cvat.exceptions.ArgumentError} * @instance
*/ */
labels: { createdDate: {
get: () => [...data.labels], get: () => data.created_date,
set: (labels) => { },
if (!Array.isArray(labels)) { /**
throw new ArgumentError('Value must be an array of Labels'); * @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))) { if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) {
throw new ArgumentError( throw new ArgumentError(
`Each array value must be an instance of Label. ${typeof label} was found`, `Each array value must be an instance of Label. ${typeof label} was found`,
); );
} }
const IDs = labels.map((_label) => _label.id); const IDs = labels.map((_label) => _label.id);
const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id)); const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id));
deletedLabels.forEach((_label) => { deletedLabels.forEach((_label) => {
_label.deleted = true; _label.deleted = true;
}); });
data.labels = [...deletedLabels, ...labels]; data.labels = [...deletedLabels, ...labels];
updateTrigger.update('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],
}, },
/** },
* Training project associated with this annotation project /**
* This is a simple object which contains * Subsets array for related tasks
* keys like host, username, password, enabled, project_class * @name subsets
* @name trainingProject * @type {string[]}
* @type {object} * @memberof module:API.cvat.classes.Project
* @memberof module:API.cvat.classes.Project * @readonly
* @readonly * @instance
* @instance */
*/ subsets: {
trainingProject: { get: () => [...data.task_subsets],
get: () => { },
if (typeof data.training_project === 'object') { /**
return { ...data.training_project }; * Training project associated with this annotation project
} * This is a simple object which contains
return data.training_project; * keys like host, username, password, enabled, project_class
}, * @name trainingProject
set: (updatedProject) => { * @type {object}
if (typeof training === 'object') { * @memberof module:API.cvat.classes.Project
data.training_project = { ...updatedProject }; * @readonly
} else { * @instance
data.training_project = updatedProject; */
} trainingProject: {
updateTrigger.update('trainingProject'); get: () => {
}, if (typeof data.training_project === 'object') {
}, return { ...data.training_project };
_internalData: { }
get: () => data, return data.training_project;
}, },
_updateTrigger: { set: (updatedProject) => {
get: () => updateTrigger, 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() // When we call a function, for example: project.annotations.get()
// In the method get we lose the project context // In the method get we lose the project context
// So, we need return it // So, we need return it
this.annotations = { this.annotations = {
exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this),
importDataset: Object.getPrototypeOf(this).annotations.importDataset.bind(this), importDataset: Object.getPrototypeOf(this).annotations.importDataset.bind(this),
}; };
} }
/** /**
* Get the first frame of the first task of a project for preview * Get the first frame of the first task of a project for preview
* @method preview * @method preview
* @memberof Project * @memberof Project
* @returns {string} - jpeg encoded image * @returns {string} - jpeg encoded image
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
async preview() { async preview() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.preview); const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.preview);
return result; return result;
} }
/** /**
* Method updates data of a created project or creates new project from scratch * Method updates data of a created project or creates new project from scratch
* @method save * @method save
* @returns {module:API.cvat.classes.Project} * @returns {module:API.cvat.classes.Project}
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @readonly * @readonly
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async save() { async save() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save); const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save);
return result; return result;
} }
/** /**
* Method deletes a project from a server * Method deletes a project from a server
* @method delete * @method delete
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @readonly * @readonly
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async delete() { async delete() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete); const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete);
return result; return result;
} }
/** /**
* Method makes a backup of a project * Method makes a backup of a project
* @method export * @method backup
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @readonly * @readonly
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @returns {string} URL to get result archive * @returns {string} URL to get result archive
*/ */
async backup() { async backup(targetStorage: Storage, useDefaultSettings: boolean, fileName?: string) {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.backup); const result = await PluginRegistry.apiWrapper.call(
return result; this,
} Project.prototype.backup,
targetStorage,
useDefaultSettings,
fileName,
);
return result;
}
/** /**
* Method restores a project from a backup * Method restores a project from a backup
* @method restore * @method restore
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @readonly * @readonly
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @returns {number} ID of the imported project * @returns {number} ID of the imported project
*/ */
static async restore(file) { static async restore(storage: Storage, file: File | string) {
const result = await PluginRegistry.apiWrapper.call(this, Project.restore, file); const result = await PluginRegistry.apiWrapper.call(this, Project.restore, storage, file);
return result; return result;
}
} }
}
Object.defineProperties( Object.defineProperties(
Project.prototype, Project.prototype,
Object.freeze({ Object.freeze({
annotations: Object.freeze({ annotations: Object.freeze({
value: { value: {
async exportDataset(format, saveImages, customName = '') { async exportDataset(
const result = await PluginRegistry.apiWrapper.call( format: string,
this, saveImages: boolean,
Project.prototype.annotations.exportDataset, useDefaultSettings: boolean,
format, targetStorage: Storage,
saveImages, customName?: string,
customName, ) {
); const result = await PluginRegistry.apiWrapper.call(
return result; this,
}, Project.prototype.annotations.exportDataset,
async importDataset(format, file, updateStatusCallback = null) { format,
const result = await PluginRegistry.apiWrapper.call( saveImages,
this, useDefaultSettings,
Project.prototype.annotations.importDataset, targetStorage,
format, customName,
file, );
updateStatusCallback, return result;
);
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,
};
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -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,
} : {}),
};
}
}

@ -1,17 +1,20 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corp // Copyright (C) 2022 CVAT.ai Corp
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api
window.cvat = require('../../src/api'); window.cvat = require('../../src/api');
const serverProxy = require('../../src/server-proxy'); const serverProxy = require('../../src/server-proxy').default;
// Test cases // Test cases
describe('Feature: get annotations', () => { describe('Feature: get annotations', () => {

@ -1,11 +1,14 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api

@ -1,11 +1,14 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api

@ -1,11 +1,14 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api

@ -1,11 +1,14 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api

@ -1,11 +1,14 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api

@ -1,17 +1,20 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api
window.cvat = require('../../src/api'); window.cvat = require('../../src/api');
const { Project } = require('../../src/project'); const Project = require('../../src/project').default;
describe('Feature: get projects', () => { describe('Feature: get projects', () => {
test('get all projects', async () => { test('get all projects', async () => {

@ -1,11 +1,14 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api
@ -23,51 +26,51 @@ describe('Feature: get info about cvat', () => {
}); });
}); });
describe('Feature: get share storage info', () => { // describe('Feature: get share storage info', () => {
test('get files in a root of a share storage', async () => { // test('get files in a root of a share storage', async () => {
const result = await window.cvat.server.share(); // const result = await window.cvat.server.share();
expect(Array.isArray(result)).toBeTruthy(); // expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(5); // expect(result).toHaveLength(5);
}); // });
test('get files in a some dir of a share storage', async () => { // test('get files in a some dir of a share storage', async () => {
const result = await window.cvat.server.share('images'); // const result = await window.cvat.server.share('images');
expect(Array.isArray(result)).toBeTruthy(); // expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(8); // expect(result).toHaveLength(8);
}); // });
test('get files in a some unknown dir of a share storage', async () => { // 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); // expect(window.cvat.server.share('Unknown Directory')).rejects.toThrow(window.cvat.exceptions.ServerError);
}); // });
}); // });
describe('Feature: get annotation formats', () => { // describe('Feature: get annotation formats', () => {
test('get annotation formats from a server', async () => { // test('get annotation formats from a server', async () => {
const result = await window.cvat.server.formats(); // const result = await window.cvat.server.formats();
expect(result).toBeInstanceOf(AnnotationFormats); // expect(result).toBeInstanceOf(AnnotationFormats);
}); // });
}); // });
describe('Feature: get annotation loaders', () => { // describe('Feature: get annotation loaders', () => {
test('get annotation formats from a server', async () => { // test('get annotation formats from a server', async () => {
const result = await window.cvat.server.formats(); // const result = await window.cvat.server.formats();
expect(result).toBeInstanceOf(AnnotationFormats); // expect(result).toBeInstanceOf(AnnotationFormats);
const { loaders } = result; // const { loaders } = result;
expect(Array.isArray(loaders)).toBeTruthy(); // expect(Array.isArray(loaders)).toBeTruthy();
for (const loader of loaders) { // for (const loader of loaders) {
expect(loader).toBeInstanceOf(Loader); // expect(loader).toBeInstanceOf(Loader);
} // }
}); // });
}); // });
describe('Feature: get annotation dumpers', () => { // describe('Feature: get annotation dumpers', () => {
test('get annotation formats from a server', async () => { // test('get annotation formats from a server', async () => {
const result = await window.cvat.server.formats(); // const result = await window.cvat.server.formats();
expect(result).toBeInstanceOf(AnnotationFormats); // expect(result).toBeInstanceOf(AnnotationFormats);
const { dumpers } = result; // const { dumpers } = result;
expect(Array.isArray(dumpers)).toBeTruthy(); // expect(Array.isArray(dumpers)).toBeTruthy();
for (const dumper of dumpers) { // for (const dumper of dumpers) {
expect(dumper).toBeInstanceOf(Dumper); // expect(dumper).toBeInstanceOf(Dumper);
} // }
}); // });
}); // });

@ -1,11 +1,14 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api

@ -1,11 +1,14 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock'); return {
return mock; __esModule: true,
default: require('../mocks/server-proxy.mock'),
};
}); });
// Initialize api // Initialize api

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.41.5", "version": "1.42.0",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -50,7 +50,7 @@ function getStore(): Store<CombinedState> {
return store; return store;
} }
function receiveAnnotationsParameters(): AnnotationsParameters { export function receiveAnnotationsParameters(): AnnotationsParameters {
if (store === null) { if (store === null) {
store = getCVATStore(); store = getCVATStore();
} }
@ -89,7 +89,7 @@ export function computeZRange(states: any[]): number[] {
return [minZ, maxZ]; return [minZ, maxZ];
} }
async function jobInfoGenerator(job: any): Promise<Record<string, number>> { export async function jobInfoGenerator(job: any): Promise<Record<string, number>> {
const { total } = await job.annotations.statistics(); const { total } = await job.annotations.statistics();
return { return {
'frame count': job.stopFrame - job.startFrame + 1, '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<Dispatch>): Promise<void> => {
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 { export function collectStatisticsAsync(sessionInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {

@ -1,51 +1,130 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { getCore, Storage } from 'cvat-core-wrapper';
const core = getCore();
export enum ExportActionTypes { export enum ExportActionTypes {
OPEN_EXPORT_MODAL = 'OPEN_EXPORT_MODAL', OPEN_EXPORT_DATASET_MODAL = 'OPEN_EXPORT_DATASET_MODAL',
CLOSE_EXPORT_MODAL = 'CLOSE_EXPORT_MODAL', CLOSE_EXPORT_DATASET_MODAL = 'CLOSE_EXPORT_DATASET_MODAL',
EXPORT_DATASET = 'EXPORT_DATASET', EXPORT_DATASET = 'EXPORT_DATASET',
EXPORT_DATASET_SUCCESS = 'EXPORT_DATASET_SUCCESS', EXPORT_DATASET_SUCCESS = 'EXPORT_DATASET_SUCCESS',
EXPORT_DATASET_FAILED = 'EXPORT_DATASET_FAILED', 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 = { export const exportActions = {
openExportModal: (instance: any) => createAction(ExportActionTypes.OPEN_EXPORT_MODAL, { instance }), openExportDatasetModal: (instance: any) => (
closeExportModal: () => createAction(ExportActionTypes.CLOSE_EXPORT_MODAL), createAction(ExportActionTypes.OPEN_EXPORT_DATASET_MODAL, { instance })
),
closeExportDatasetModal: (instance: any) => (
createAction(ExportActionTypes.CLOSE_EXPORT_DATASET_MODAL, { instance })
),
exportDataset: (instance: any, format: string) => ( exportDataset: (instance: any, format: string) => (
createAction(ExportActionTypes.EXPORT_DATASET, { instance, format }) createAction(ExportActionTypes.EXPORT_DATASET, { instance, format })
), ),
exportDatasetSuccess: (instance: any, format: string) => ( exportDatasetSuccess: (
createAction(ExportActionTypes.EXPORT_DATASET_SUCCESS, { instance, format }) 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, { createAction(ExportActionTypes.EXPORT_DATASET_FAILED, {
instance, instance,
instanceType,
format, format,
error, 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 = ( export const exportDatasetAsync = (
instance: any, instance: any,
format: string, format: string,
name: string,
saveImages: boolean, saveImages: boolean,
useDefaultSettings: boolean,
targetStorage: Storage,
name?: string,
): ThunkAction => async (dispatch) => { ): ThunkAction => async (dispatch) => {
dispatch(exportActions.exportDataset(instance, format)); 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 { try {
const url = await instance.annotations.exportDataset(format, saveImages, name); const result = await instance.backup(targetStorage, useDefaultSetting, fileName);
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement; if (result) {
downloadAnchor.href = url; const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.click(); downloadAnchor.href = result;
dispatch(exportActions.exportDatasetSuccess(instance, format)); downloadAnchor.click();
}
dispatch(exportActions.exportBackupSuccess(instance, instanceType, !!result));
} catch (error) { } catch (error) {
dispatch(exportActions.exportDatasetFailed(instance, format, error)); dispatch(exportActions.exportBackupFailed(instance, instanceType, error as Error));
} }
}; };

@ -1,58 +1,167 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { createAction, ActionUnion, ThunkAction } from 'utils/redux'; import { createAction, ActionUnion, ThunkAction } from 'utils/redux';
import { CombinedState } from 'reducers'; import { CombinedState } from 'reducers';
import { getCore, Storage } from 'cvat-core-wrapper';
import { LogType } from 'cvat-logger';
import { getProjectsAsync } from './projects-actions'; import { getProjectsAsync } from './projects-actions';
import { jobInfoGenerator, receiveAnnotationsParameters, AnnotationActionTypes } from './annotation-actions';
const core = getCore();
export enum ImportActionTypes { export enum ImportActionTypes {
OPEN_IMPORT_MODAL = 'OPEN_IMPORT_MODAL', OPEN_IMPORT_DATASET_MODAL = 'OPEN_IMPORT_DATASET_MODAL',
CLOSE_IMPORT_MODAL = 'CLOSE_IMPORT_MODAL', CLOSE_IMPORT_DATASET_MODAL = 'CLOSE_IMPORT_DATASET_MODAL',
IMPORT_DATASET = 'IMPORT_DATASET', IMPORT_DATASET = 'IMPORT_DATASET',
IMPORT_DATASET_SUCCESS = 'IMPORT_DATASET_SUCCESS', IMPORT_DATASET_SUCCESS = 'IMPORT_DATASET_SUCCESS',
IMPORT_DATASET_FAILED = 'IMPORT_DATASET_FAILED', IMPORT_DATASET_FAILED = 'IMPORT_DATASET_FAILED',
IMPORT_DATASET_UPDATE_STATUS = 'IMPORT_DATASET_UPDATE_STATUS', 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 = { export const importActions = {
openImportModal: (instance: any) => createAction(ImportActionTypes.OPEN_IMPORT_MODAL, { instance }), openImportDatasetModal: (instance: any) => (
closeImportModal: () => createAction(ImportActionTypes.CLOSE_IMPORT_MODAL), createAction(ImportActionTypes.OPEN_IMPORT_DATASET_MODAL, { instance })
importDataset: (projectId: number) => ( ),
createAction(ImportActionTypes.IMPORT_DATASET, { id: projectId }) closeImportDatasetModal: (instance: any) => (
createAction(ImportActionTypes.CLOSE_IMPORT_DATASET_MODAL, { instance })
),
importDataset: (instance: any, format: string) => (
createAction(ImportActionTypes.IMPORT_DATASET, { instance, format })
), ),
importDatasetSuccess: () => ( importDatasetSuccess: (instance: any, resource: 'dataset' | 'annotation') => (
createAction(ImportActionTypes.IMPORT_DATASET_SUCCESS) 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, { createAction(ImportActionTypes.IMPORT_DATASET_FAILED, {
instance, instance,
resource,
error, error,
}) })
), ),
importDatasetUpdateStatus: (progress: number, status: string) => ( importDatasetUpdateStatus: (instance: any, progress: number, status: string) => (
createAction(ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS, { progress, status }) 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) => { async (dispatch, getState) => {
const resource = instance instanceof core.classes.Project ? 'dataset' : 'annotation';
try { try {
const state: CombinedState = getState(); 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) { } catch (error) {
dispatch(importActions.importDatasetFailed(instance, error)); dispatch(importActions.importDatasetFailed(instance, resource, error));
return; return;
} }
dispatch(importActions.importDatasetSuccess()); dispatch(importActions.importDatasetSuccess(instance, resource));
dispatch(getProjectsAsync({ id: instance.id }, getState().projects.tasksGettingQuery)); 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));
}
} }
); );

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -28,12 +29,6 @@ export enum ProjectsActionTypes {
DELETE_PROJECT = 'DELETE_PROJECT', DELETE_PROJECT = 'DELETE_PROJECT',
DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS', DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS',
DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED', 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 // prettier-ignore
@ -63,20 +58,6 @@ const projectActions = {
deleteProjectFailed: (projectId: number, error: any) => ( deleteProjectFailed: (projectId: number, error: any) => (
createAction(ProjectsActionTypes.DELETE_PROJECT_FAILED, { projectId, error }) 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<typeof projectActions>; export type ProjectActions = ActionUnion<typeof projectActions>;
@ -190,31 +171,3 @@ export function deleteProjectAsync(projectInstance: any): ThunkAction {
} }
}; };
} }
export function restoreProjectAsync(file: File): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
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<Dispatch>): Promise<void> => {
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));
}
};
}

@ -1,12 +1,14 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import { TasksQuery, CombinedState, Indexable } from 'reducers'; import {
import { getCVATStore } from 'cvat-store'; TasksQuery, CombinedState, Indexable, StorageLocation,
import { getCore } from 'cvat-core-wrapper'; } from 'reducers';
import { getCore, Storage } from 'cvat-core-wrapper';
import { getInferenceStatusAsync } from './models-actions'; import { getInferenceStatusAsync } from './models-actions';
const cvat = getCore(); const cvat = getCore();
@ -15,9 +17,6 @@ export enum TasksActionTypes {
GET_TASKS = 'GET_TASKS', GET_TASKS = 'GET_TASKS',
GET_TASKS_SUCCESS = 'GET_TASKS_SUCCESS', GET_TASKS_SUCCESS = 'GET_TASKS_SUCCESS',
GET_TASKS_FAILED = 'GET_TASKS_FAILED', 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 = 'DELETE_TASK',
DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS', DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS',
DELETE_TASK_FAILED = 'DELETE_TASK_FAILED', DELETE_TASK_FAILED = 'DELETE_TASK_FAILED',
@ -32,12 +31,6 @@ export enum TasksActionTypes {
UPDATE_JOB_SUCCESS = 'UPDATE_JOB_SUCCESS', UPDATE_JOB_SUCCESS = 'UPDATE_JOB_SUCCESS',
UPDATE_JOB_FAILED = 'UPDATE_JOB_FAILED', UPDATE_JOB_FAILED = 'UPDATE_JOB_FAILED',
HIDE_EMPTY_TASKS = 'HIDE_EMPTY_TASKS', 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', 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<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
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<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
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<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
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 { function deleteTask(taskID: number): AnyAction {
const action = { const action = {
type: TasksActionTypes.DELETE_TASK, type: TasksActionTypes.DELETE_TASK,
@ -353,6 +195,8 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
use_zip_chunks: data.advanced.useZipChunks, use_zip_chunks: data.advanced.useZipChunks,
use_cache: data.advanced.useCache, use_cache: data.advanced.useCache,
sorting_method: data.advanced.sortingMethod, 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) { if (data.projectId) {

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -9,8 +10,6 @@ import Modal from 'antd/lib/modal';
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface'; import { MenuInfo } from 'rc-menu/lib/interface';
import LoadSubmenu from './load-submenu';
import { DimensionType } from '../../reducers'; import { DimensionType } from '../../reducers';
interface Props { interface Props {
@ -19,12 +18,10 @@ interface Props {
bugTracker: string; bugTracker: string;
loaders: any[]; loaders: any[];
dumpers: any[]; dumpers: any[];
loadActivity: string | null;
inferenceIsActive: boolean; inferenceIsActive: boolean;
taskDimension: DimensionType; taskDimension: DimensionType;
backupIsActive: boolean;
onClickMenu: (params: MenuInfo) => void; onClickMenu: (params: MenuInfo) => void;
onUploadAnnotations: (format: string, file: File) => void;
exportIsActive: boolean;
} }
export enum Actions { export enum Actions {
@ -34,7 +31,7 @@ export enum Actions {
RUN_AUTO_ANNOTATION = 'run_auto_annotation', RUN_AUTO_ANNOTATION = 'run_auto_annotation',
MOVE_TASK_TO_PROJECT = 'move_task_to_project', MOVE_TASK_TO_PROJECT = 'move_task_to_project',
OPEN_BUG_TRACKER = 'open_bug_tracker', OPEN_BUG_TRACKER = 'open_bug_tracker',
EXPORT_TASK = 'export_task', BACKUP_TASK = 'backup_task',
} }
function ActionsMenuComponent(props: Props): JSX.Element { function ActionsMenuComponent(props: Props): JSX.Element {
@ -42,12 +39,8 @@ function ActionsMenuComponent(props: Props): JSX.Element {
taskID, taskID,
bugTracker, bugTracker,
inferenceIsActive, inferenceIsActive,
loaders, backupIsActive,
onClickMenu, onClickMenu,
onUploadAnnotations,
loadActivity,
taskDimension,
exportIsActive,
} = props; } = props;
const onClickMenuWrapper = useCallback( const onClickMenuWrapper = useCallback(
@ -79,38 +72,16 @@ function ActionsMenuComponent(props: Props): JSX.Element {
return ( return (
<Menu selectable={false} className='cvat-actions-menu' onClick={onClickMenuWrapper}> <Menu selectable={false} className='cvat-actions-menu' onClick={onClickMenuWrapper}>
{LoadSubmenu({ <Menu.Item key={Actions.LOAD_TASK_ANNO}>Upload annotations</Menu.Item>
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,
})}
<Menu.Item key={Actions.EXPORT_TASK_DATASET}>Export task dataset</Menu.Item> <Menu.Item key={Actions.EXPORT_TASK_DATASET}>Export task dataset</Menu.Item>
{!!bugTracker && <Menu.Item key={Actions.OPEN_BUG_TRACKER}>Open bug tracker</Menu.Item>} {!!bugTracker && <Menu.Item key={Actions.OPEN_BUG_TRACKER}>Open bug tracker</Menu.Item>}
<Menu.Item disabled={inferenceIsActive} key={Actions.RUN_AUTO_ANNOTATION}> <Menu.Item disabled={inferenceIsActive} key={Actions.RUN_AUTO_ANNOTATION}>
Automatic annotation Automatic annotation
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
key={Actions.EXPORT_TASK} key={Actions.BACKUP_TASK}
disabled={exportIsActive} disabled={backupIsActive}
icon={exportIsActive && <LoadingOutlined id='cvat-export-task-loading' />} icon={backupIsActive && <LoadingOutlined id='cvat-backup-task-loading' />}
> >
Backup Task Backup Task
</Menu.Item> </Menu.Item>

@ -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 (
<Menu.SubMenu key={menuKey} title='Upload annotations'>
{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 (
<Menu.Item key={format} disabled={disabled} className='cvat-menu-load-submenu-item'>
<Upload
accept={accept}
multiple={false}
showUploadList={false}
beforeUpload={(file: File): boolean => {
onFileUpload(format, file);
return false;
}}
>
<Button
block
type='link'
disabled={disabled}
className='cvat-menu-load-submenu-item-button'
>
<UploadOutlined />
<Text>{loader.name}</Text>
{pending && <LoadingOutlined style={{ marginLeft: 10 }} />}
</Button>
</Upload>
</Menu.Item>
);
},
)}
</Menu.SubMenu>
);
}

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -17,7 +18,6 @@
} }
} }
.cvat-menu-load-submenu-item,
.cvat-menu-dump-submenu-item, .cvat-menu-dump-submenu-item,
.cvat-menu-export-submenu-item { .cvat-menu-export-submenu-item {
> span[role='img'] { > 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 { .cvat-menu-icon {
font-size: 16px; font-size: 16px;
margin-left: 8px; margin-left: 8px;
align-self: center; align-self: center;
} }
#cvat-export-task-loading { #cvat-backup-task-loading {
margin-left: 10; margin-left: 10;
} }

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -14,9 +15,7 @@ import Collapse from 'antd/lib/collapse';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface'; import { MenuInfo } from 'rc-menu/lib/interface';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import LoadSubmenu from 'components/actions-menu/load-submenu';
import { getCore } from 'cvat-core-wrapper'; import { getCore } from 'cvat-core-wrapper';
import { JobStage } from 'reducers'; import { JobStage } from 'reducers';
@ -24,12 +23,8 @@ const core = getCore();
interface Props { interface Props {
taskMode: string; taskMode: string;
loaders: any[];
dumpers: any[];
loadActivity: string | null;
jobInstance: any; jobInstance: any;
onClickMenu(params: MenuInfo): void; onClickMenu(params: MenuInfo): void;
onUploadAnnotations(format: string, file: File): void;
stopFrame: number; stopFrame: number;
removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean): void; removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean): void;
setForceExitAnnotationFlag(forceExit: boolean): void; setForceExitAnnotationFlag(forceExit: boolean): void;
@ -38,7 +33,7 @@ interface Props {
export enum Actions { export enum Actions {
LOAD_JOB_ANNO = 'load_job_anno', LOAD_JOB_ANNO = 'load_job_anno',
EXPORT_TASK_DATASET = 'export_task_dataset', EXPORT_JOB_DATASET = 'export_job_dataset',
REMOVE_ANNO = 'remove_anno', REMOVE_ANNO = 'remove_anno',
OPEN_TASK = 'open_task', OPEN_TASK = 'open_task',
FINISH_JOB = 'finish_job', FINISH_JOB = 'finish_job',
@ -47,13 +42,10 @@ export enum Actions {
function AnnotationMenuComponent(props: Props & RouteComponentProps): JSX.Element { function AnnotationMenuComponent(props: Props & RouteComponentProps): JSX.Element {
const { const {
loaders,
loadActivity,
jobInstance, jobInstance,
stopFrame, stopFrame,
history, history,
onClickMenu, onClickMenu,
onUploadAnnotations,
removeAnnotations, removeAnnotations,
setForceExitAnnotationFlag, setForceExitAnnotationFlag,
saveAnnotations, saveAnnotations,
@ -192,30 +184,8 @@ function AnnotationMenuComponent(props: Props & RouteComponentProps): JSX.Elemen
return ( return (
<Menu onClick={(params: MenuInfo) => onClickMenuWrapper(params)} className='cvat-annotation-menu' selectable={false}> <Menu onClick={(params: MenuInfo) => onClickMenuWrapper(params)} className='cvat-annotation-menu' selectable={false}>
{LoadSubmenu({ <Menu.Item key={Actions.LOAD_JOB_ANNO}>Upload annotations</Menu.Item>
loaders, <Menu.Item key={Actions.EXPORT_JOB_DATASET}>Export job dataset</Menu.Item>
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,
})}
<Menu.Item key={Actions.EXPORT_TASK_DATASET}>Export task dataset</Menu.Item>
<Menu.Item key={Actions.REMOVE_ANNO}>Remove annotations</Menu.Item> <Menu.Item key={Actions.REMOVE_ANNO}>Remove annotations</Menu.Item>
<Menu.Item key={Actions.OPEN_TASK}> <Menu.Item key={Actions.OPEN_TASK}>
<a <a

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -12,17 +13,47 @@ import Select from 'antd/lib/select';
import { Col, Row } from 'antd/lib/grid'; import { Col, Row } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import Form, { FormInstance } from 'antd/lib/form'; import Form, { FormInstance } from 'antd/lib/form';
import Collapse from 'antd/lib/collapse';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import { StorageLocation } from 'reducers';
import { createProjectAsync } from 'actions/projects-actions';
import { Storage, StorageData } from 'cvat-core-wrapper';
import patterns from 'utils/validation-patterns'; import patterns from 'utils/validation-patterns';
import LabelsEditor from 'components/labels-editor/labels-editor'; import LabelsEditor from 'components/labels-editor/labels-editor';
import { createProjectAsync } from 'actions/projects-actions'; import SourceStorageField from 'components/storage/source-storage-field';
import TargetStorageField from 'components/storage/target-storage-field';
import CreateProjectContext from './create-project.context'; import CreateProjectContext from './create-project.context';
const { Option } = Select; const { Option } = Select;
interface AdvancedConfiguration {
sourceStorage: StorageData;
targetStorage: StorageData;
bug_tracker?: string | null;
}
const initialValues: AdvancedConfiguration = {
bug_tracker: null,
sourceStorage: {
location: StorageLocation.LOCAL,
cloudStorageId: undefined,
},
targetStorage: {
location: StorageLocation.LOCAL,
cloudStorageId: undefined,
},
};
interface AdvancedConfigurationProps {
formRef: RefObject<FormInstance>;
sourceStorageLocation: StorageLocation;
targetStorageLocation: StorageLocation;
onChangeSourceStorageLocation?: (value: StorageLocation) => void;
onChangeTargetStorageLocation?: (value: StorageLocation) => void;
}
function NameConfigurationForm( function NameConfigurationForm(
{ formRef, inputRef }: { formRef, inputRef }:
{ formRef: RefObject<FormInstance>, inputRef: RefObject<Input> }, { formRef: RefObject<FormInstance>, inputRef: RefObject<Input> },
@ -99,9 +130,16 @@ function AdaptiveAutoAnnotationForm({ formRef }: { formRef: RefObject<FormInstan
); );
} }
function AdvancedConfigurationForm({ formRef }: { formRef: RefObject<FormInstance> }): JSX.Element { function AdvancedConfigurationForm(props: AdvancedConfigurationProps): JSX.Element {
const {
formRef,
sourceStorageLocation,
targetStorageLocation,
onChangeSourceStorageLocation,
onChangeTargetStorageLocation,
} = props;
return ( return (
<Form layout='vertical' ref={formRef}> <Form layout='vertical' ref={formRef} initialValues={initialValues}>
<Form.Item <Form.Item
name='bug_tracker' name='bug_tracker'
label='Issue tracker' label='Issue tracker'
@ -121,12 +159,32 @@ function AdvancedConfigurationForm({ formRef }: { formRef: RefObject<FormInstanc
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Row justify='space-between'>
<Col span={11}>
<SourceStorageField
instanceId={null}
storageDescription='Specify source storage for import resources like annotation, backups'
locationValue={sourceStorageLocation}
onChangeLocationValue={onChangeSourceStorageLocation}
/>
</Col>
<Col span={11} offset={1}>
<TargetStorageField
instanceId={null}
storageDescription='Specify target storage for export resources like annotation, backups'
locationValue={targetStorageLocation}
onChangeLocationValue={onChangeTargetStorageLocation}
/>
</Col>
</Row>
</Form> </Form>
); );
} }
export default function CreateProjectContent(): JSX.Element { export default function CreateProjectContent(): JSX.Element {
const [projectLabels, setProjectLabels] = useState<any[]>([]); const [projectLabels, setProjectLabels] = useState<any[]>([]);
const [sourceStorageLocation, setSourceStorageLocation] = useState(StorageLocation.LOCAL);
const [targetStorageLocation, setTargetStorageLocation] = useState(StorageLocation.LOCAL);
const nameFormRef = useRef<FormInstance>(null); const nameFormRef = useRef<FormInstance>(null);
const nameInputRef = useRef<Input>(null); const nameInputRef = useRef<Input>(null);
const adaptiveAutoAnnotationFormRef = useRef<FormInstance>(null); const adaptiveAutoAnnotationFormRef = useRef<FormInstance>(null);
@ -140,23 +198,32 @@ export default function CreateProjectContent(): JSX.Element {
if (nameFormRef.current) nameFormRef.current.resetFields(); if (nameFormRef.current) nameFormRef.current.resetFields();
if (advancedFormRef.current) advancedFormRef.current.resetFields(); if (advancedFormRef.current) advancedFormRef.current.resetFields();
setProjectLabels([]); setProjectLabels([]);
setSourceStorageLocation(StorageLocation.LOCAL);
setTargetStorageLocation(StorageLocation.LOCAL);
}; };
const focusForm = (): void => { const focusForm = (): void => {
nameInputRef.current?.focus(); nameInputRef.current?.focus();
}; };
const sumbit = async (): Promise<any> => { const submit = async (): Promise<any> => {
try { try {
let projectData: Record<string, any> = {}; let projectData: Record<string, any> = {};
if (nameFormRef.current && advancedFormRef.current) { if (nameFormRef.current) {
const basicValues = await nameFormRef.current.validateFields(); 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(); const adaptiveAutoAnnotationValues = await adaptiveAutoAnnotationFormRef.current?.validateFields();
projectData = { projectData = {
...projectData, ...projectData,
...advancedValues, ...advancedValues,
name: basicValues.name, 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) { if (adaptiveAutoAnnotationValues) {
@ -174,14 +241,14 @@ export default function CreateProjectContent(): JSX.Element {
}; };
const onSubmitAndOpen = async (): Promise<void> => { const onSubmitAndOpen = async (): Promise<void> => {
const createdProject = await sumbit(); const createdProject = await submit();
if (createdProject) { if (createdProject) {
history.push(`/projects/${createdProject.id}`); history.push(`/projects/${createdProject.id}`);
} }
}; };
const onSubmitAndContinue = async (): Promise<void> => { const onSubmitAndContinue = async (): Promise<void> => {
const res = await sumbit(); const res = await submit();
if (res) { if (res) {
resetForm(); resetForm();
notification.info({ notification.info({
@ -216,7 +283,17 @@ export default function CreateProjectContent(): JSX.Element {
/> />
</Col> </Col>
<Col span={24}> <Col span={24}>
<AdvancedConfigurationForm formRef={advancedFormRef} /> <Collapse>
<Collapse.Panel key='1' header={<Text className='cvat-title'>Advanced configuration</Text>}>
<AdvancedConfigurationForm
formRef={advancedFormRef}
sourceStorageLocation={sourceStorageLocation}
targetStorageLocation={targetStorageLocation}
onChangeSourceStorageLocation={(value: StorageLocation) => setSourceStorageLocation(value)}
onChangeTargetStorageLocation={(value: StorageLocation) => setTargetStorageLocation(value)}
/>
</Collapse.Panel>
</Collapse>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Row justify='end' gutter={5}> <Row justify='end' gutter={5}>

@ -1,12 +1,16 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React, { RefObject } from 'react'; import React, { RefObject } from 'react';
import { Row, Col } from 'antd/lib/grid'; 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 Input from 'antd/lib/input';
import Select from 'antd/lib/select'; 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 Radio from 'antd/lib/radio';
import Checkbox from 'antd/lib/checkbox'; import Checkbox from 'antd/lib/checkbox';
import Form, { FormInstance, RuleObject, RuleRender } from 'antd/lib/form'; 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 { Store } from 'antd/lib/form/interface';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import patterns from 'utils/validation-patterns'; 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; const { Option } = Select;
@ -40,6 +51,10 @@ export interface AdvancedConfiguration {
useCache: boolean; useCache: boolean;
copyData?: boolean; copyData?: boolean;
sortingMethod: SortingMethod; sortingMethod: SortingMethod;
useProjectSourceStorage: boolean;
useProjectTargetStorage: boolean;
sourceStorage: StorageData;
targetStorage: StorageData;
} }
const initialValues: AdvancedConfiguration = { const initialValues: AdvancedConfiguration = {
@ -49,13 +64,33 @@ const initialValues: AdvancedConfiguration = {
useCache: true, useCache: true,
copyData: false, copyData: false,
sortingMethod: SortingMethod.LEXICOGRAPHICAL, sortingMethod: SortingMethod.LEXICOGRAPHICAL,
useProjectSourceStorage: true,
useProjectTargetStorage: true,
sourceStorage: {
location: StorageLocation.LOCAL,
cloudStorageId: undefined,
},
targetStorage: {
location: StorageLocation.LOCAL,
cloudStorageId: undefined,
},
}; };
interface Props { interface Props {
onSubmit(values: AdvancedConfiguration): void; onSubmit(values: AdvancedConfiguration): void;
onChangeUseProjectSourceStorage(value: boolean): void;
onChangeUseProjectTargetStorage(value: boolean): void;
onChangeSourceStorageLocation: (value: StorageLocation) => void;
onChangeTargetStorageLocation: (value: StorageLocation) => void;
installedGit: boolean; installedGit: boolean;
projectId: number | null;
useProjectSourceStorage: boolean;
useProjectTargetStorage: boolean;
activeFileManagerTab: string; activeFileManagerTab: string;
dumpers: [] dumpers: [];
sourceStorageLocation: StorageLocation;
targetStorageLocation: StorageLocation;
} }
function validateURL(_: RuleObject, value: string): Promise<void> { function validateURL(_: RuleObject, value: string): Promise<void> {
@ -146,10 +181,15 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
} }
public submit(): Promise<void> { public submit(): Promise<void> {
const { onSubmit } = this.props; const { onSubmit, projectId } = this.props;
if (this.formRef.current) { if (this.formRef.current) {
return this.formRef.current.validateFields().then( if (projectId) {
(values: Store): Promise<void> => { 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 frameFilter = values.frameStep ? `step=${values.frameStep}` : undefined;
const entries = Object.entries(values).filter( const entries = Object.entries(values).filter(
(entry: [string, unknown]): boolean => entry[0] !== frameFilter, (entry: [string, unknown]): boolean => entry[0] !== frameFilter,
@ -158,10 +198,33 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
onSubmit({ onSubmit({
...((Object.fromEntries(entries) as any) as AdvancedConfiguration), ...((Object.fromEntries(entries) as any) as AdvancedConfiguration),
frameFilter, 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 Promise.resolve();
}, });
); }
return this.formRef.current.validateFields()
.then(
(values: Store): Promise<void> => {
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')); return Promise.reject(new Error('Form ref is empty'));
@ -201,15 +264,15 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
]} ]}
help='Specify how to sort images. It is not relevant for videos.' help='Specify how to sort images. It is not relevant for videos.'
> >
<Radio.Group> <Radio.Group buttonStyle='solid'>
<Radio value={SortingMethod.LEXICOGRAPHICAL} key={SortingMethod.LEXICOGRAPHICAL}> <Radio.Button value={SortingMethod.LEXICOGRAPHICAL} key={SortingMethod.LEXICOGRAPHICAL}>
Lexicographical Lexicographical
</Radio> </Radio.Button>
<Radio value={SortingMethod.NATURAL} key={SortingMethod.NATURAL}>Natural</Radio> <Radio.Button value={SortingMethod.NATURAL} key={SortingMethod.NATURAL}>Natural</Radio.Button>
<Radio value={SortingMethod.PREDEFINED} key={SortingMethod.PREDEFINED}> <Radio.Button value={SortingMethod.PREDEFINED} key={SortingMethod.PREDEFINED}>
Predefined Predefined
</Radio> </Radio.Button>
<Radio value={SortingMethod.RANDOM} key={SortingMethod.RANDOM}>Random</Radio> <Radio.Button value={SortingMethod.RANDOM} key={SortingMethod.RANDOM}>Random</Radio.Button>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
); );
@ -291,15 +354,19 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
private renderGitLFSBox(): JSX.Element { private renderGitLFSBox(): JSX.Element {
return ( return (
<Form.Item <Space>
help='If annotation files are large, you can use git LFS feature' <Form.Item
name='lfs' name='lfs'
valuePropName='checked' valuePropName='checked'
> className='cvat-settings-switch'
<Checkbox> >
<Text className='cvat-text-color'>Use LFS (Large File Support):</Text> <Switch />
</Checkbox> </Form.Item>
</Form.Item> <Text className='cvat-text-color'>Use LFS (Large File Support):</Text>
<Tooltip title='If annotation files are large, you can use git LFS feature.'>
<QuestionCircleOutlined style={{ opacity: 0.5 }} />
</Tooltip>
</Space>
); );
} }
@ -374,25 +441,37 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
private renderUzeZipChunks(): JSX.Element { private renderUzeZipChunks(): JSX.Element {
return ( return (
<Form.Item <Space>
help='Force to use zip chunks as compressed data. Cut out content for videos only.' <Form.Item
name='useZipChunks' name='useZipChunks'
valuePropName='checked' valuePropName='checked'
> className='cvat-settings-switch'
<Checkbox> >
<Text className='cvat-text-color'>Use zip/video chunks</Text> <Switch />
</Checkbox> </Form.Item>
</Form.Item> <Text className='cvat-text-color'>Use zip/video chunks</Text>
<Tooltip title='Force to use zip chunks as compressed data. Cut out content for videos only.'>
<QuestionCircleOutlined style={{ opacity: 0.5 }} />
</Tooltip>
</Space>
); );
} }
private renderCreateTaskMethod(): JSX.Element { private renderCreateTaskMethod(): JSX.Element {
return ( return (
<Form.Item help='Using cache to store data.' name='useCache' valuePropName='checked'> <Space>
<Checkbox> <Form.Item
<Text className='cvat-text-color'>Use cache</Text> name='useCache'
</Checkbox> valuePropName='checked'
</Form.Item> className='cvat-settings-switch'
>
<Switch defaultChecked />
</Form.Item>
<Text className='cvat-text-color'>Use cache</Text>
<Tooltip title='Using cache to store data.'>
<QuestionCircleOutlined style={{ opacity: 0.5 }} />
</Tooltip>
</Space>
); );
} }
@ -423,6 +502,48 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
); );
} }
private renderSourceStorage(): JSX.Element {
const {
projectId,
useProjectSourceStorage,
sourceStorageLocation,
onChangeUseProjectSourceStorage,
onChangeSourceStorageLocation,
} = this.props;
return (
<SourceStorageField
instanceId={projectId}
locationValue={sourceStorageLocation}
switchDescription='Use project source storage'
storageDescription='Specify source storage for import resources like annotation, backups'
useDefaultStorage={useProjectSourceStorage}
onChangeUseDefaultStorage={onChangeUseProjectSourceStorage}
onChangeLocationValue={onChangeSourceStorageLocation}
/>
);
}
private renderTargetStorage(): JSX.Element {
const {
projectId,
useProjectTargetStorage,
targetStorageLocation,
onChangeUseProjectTargetStorage,
onChangeTargetStorageLocation,
} = this.props;
return (
<TargetStorageField
instanceId={projectId}
locationValue={targetStorageLocation}
switchDescription='Use project target storage'
storageDescription='Specify target storage for export resources like annotation, backups '
useDefaultStorage={useProjectTargetStorage}
onChangeUseDefaultStorage={onChangeUseProjectTargetStorage}
onChangeLocationValue={onChangeTargetStorageLocation}
/>
);
}
public render(): JSX.Element { public render(): JSX.Element {
const { installedGit, activeFileManagerTab } = this.props; const { installedGit, activeFileManagerTab } = this.props;
return ( return (
@ -436,10 +557,8 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
</Row> </Row>
) : null} ) : null}
<Row> <Row>
<Col>{this.renderUzeZipChunks()}</Col> <Col span={12}>{this.renderUzeZipChunks()}</Col>
</Row> <Col span={12}>{this.renderCreateTaskMethod()}</Col>
<Row>
<Col>{this.renderCreateTaskMethod()}</Col>
</Row> </Row>
<Row justify='start'> <Row justify='start'>
<Col span={7}>{this.renderImageQuality()}</Col> <Col span={7}>{this.renderImageQuality()}</Col>
@ -470,6 +589,14 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
<Row> <Row>
<Col span={24}>{this.renderBugTracker()}</Col> <Col span={24}>{this.renderBugTracker()}</Col>
</Row> </Row>
<Row justify='space-between'>
<Col span={11}>
{this.renderSourceStorage()}
</Col>
<Col span={11} offset={1}>
{this.renderTargetStorage()}
</Col>
</Row>
</Form> </Form>
); );
} }

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -13,7 +14,8 @@ import notification from 'antd/lib/notification';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { ValidateErrorEntity } from 'rc-field-form/lib/interface'; 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 ConnectedFileManager from 'containers/file-manager/file-manager';
import LabelsEditor from 'components/labels-editor/labels-editor'; import LabelsEditor from 'components/labels-editor/labels-editor';
import { Files } from 'components/file-manager/file-manager'; 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 ProjectSubsetField from './project-subset-field';
import AdvancedConfigurationForm, { AdvancedConfiguration, SortingMethod } from './advanced-configuration-form'; import AdvancedConfigurationForm, { AdvancedConfiguration, SortingMethod } from './advanced-configuration-form';
const core = getCore();
export interface CreateTaskData { export interface CreateTaskData {
projectId: number | null; projectId: number | null;
basic: BaseConfiguration; basic: BaseConfiguration;
@ -55,6 +59,16 @@ const defaultState = {
useZipChunks: true, useZipChunks: true,
useCache: true, useCache: true,
sortingMethod: SortingMethod.LEXICOGRAPHICAL, sortingMethod: SortingMethod.LEXICOGRAPHICAL,
sourceStorage: {
location: StorageLocation.LOCAL,
cloudStorageId: undefined,
},
targetStorage: {
location: StorageLocation.LOCAL,
cloudStorageId: undefined,
},
useProjectSourceStorage: true,
useProjectTargetStorage: true,
}, },
labels: [], labels: [],
files: { files: {
@ -89,6 +103,17 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
this.focusToForm(); this.focusToForm();
} }
private handleChangeStorageLocation(field: 'sourceStorage' | 'targetStorage', value: StorageLocation): void {
this.setState((state) => ({
advanced: {
...state.advanced,
[field]: {
location: value,
},
},
}));
}
private resetState = (): void => { private resetState = (): void => {
this.basicConfigurationComponent.current?.resetFields(); this.basicConfigurationComponent.current?.resetFields();
this.advancedConfigurationComponent.current?.resetFields(); this.advancedConfigurationComponent.current?.resetFields();
@ -160,6 +185,24 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}); });
}; };
private handleUseProjectSourceStorageChange = (value: boolean): void => {
this.setState((state) => ({
advanced: {
...state.advanced,
useProjectSourceStorage: value,
},
}));
};
private handleUseProjectTargetStorageChange = (value: boolean): void => {
this.setState((state) => ({
advanced: {
...state.advanced,
useProjectTargetStorage: value,
},
}));
};
private focusToForm = (): void => { private focusToForm = (): void => {
this.basicConfigurationComponent.current?.focus(); this.basicConfigurationComponent.current?.focus();
}; };
@ -189,6 +232,8 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}; };
private handleSubmit = (): Promise<any> => new Promise((resolve, reject) => { private handleSubmit = (): Promise<any> => new Promise((resolve, reject) => {
const { projectId } = this.state;
if (!this.validateLabelsOrProject()) { if (!this.validateLabelsOrProject()) {
notification.error({ notification.error({
message: 'Could not create a task', message: 'Could not create a task',
@ -220,6 +265,26 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
if (this.advancedConfigurationComponent.current) { if (this.advancedConfigurationComponent.current) {
return this.advancedConfigurationComponent.current.submit(); return this.advancedConfigurationComponent.current.submit();
} }
if (projectId) {
return core.projects.get({ id: projectId })
.then((response: any) => {
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(); return Promise.resolve();
}) })
.then((): void => { .then((): void => {
@ -341,7 +406,20 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
private renderAdvancedBlock(): JSX.Element { private renderAdvancedBlock(): JSX.Element {
const { installedGit, dumpers } = this.props; const { installedGit, dumpers } = this.props;
const { activeFileManagerTab } = this.state; const { activeFileManagerTab, projectId } = this.state;
const {
advanced: {
useProjectSourceStorage,
useProjectTargetStorage,
sourceStorage: {
location: sourceStorageLocation,
},
targetStorage: {
location: targetStorageLocation,
},
},
} = this.state;
return ( return (
<Col span={24}> <Col span={24}>
<Collapse> <Collapse>
@ -352,6 +430,19 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
activeFileManagerTab={activeFileManagerTab} activeFileManagerTab={activeFileManagerTab}
ref={this.advancedConfigurationComponent} ref={this.advancedConfigurationComponent}
onSubmit={this.handleSubmitAdvancedConfiguration} onSubmit={this.handleSubmitAdvancedConfiguration}
projectId={projectId}
useProjectSourceStorage={useProjectSourceStorage}
useProjectTargetStorage={useProjectTargetStorage}
sourceStorageLocation={sourceStorageLocation}
targetStorageLocation={targetStorageLocation}
onChangeUseProjectSourceStorage={this.handleUseProjectSourceStorageChange}
onChangeUseProjectTargetStorage={this.handleUseProjectTargetStorageChange}
onChangeSourceStorageLocation={(value: StorageLocation) => {
this.handleChangeStorageLocation('sourceStorage', value);
}}
onChangeTargetStorageLocation={(value: StorageLocation) => {
this.handleChangeStorageLocation('targetStorage', value);
}}
/> />
</Collapse.Panel> </Collapse.Panel>
</Collapse> </Collapse>

@ -38,4 +38,8 @@
width: 100%; width: 100%;
} }
} }
.cvat-settings-switch {
display: table-cell;
}
} }

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // 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 ShortcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog';
import ExportDatasetModal from 'components/export-dataset/export-dataset-modal'; 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 ModelsPageContainer from 'containers/models-page/models-page';
import JobsPageComponent from 'components/jobs-page/jobs-page'; import JobsPageComponent from 'components/jobs-page/jobs-page';
@ -399,6 +403,9 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
</GlobalHotKeys> </GlobalHotKeys>
{/* eslint-disable-next-line */} {/* eslint-disable-next-line */}
<ExportDatasetModal /> <ExportDatasetModal />
<ExportBackupModal />
<ImportDatasetModal />
<ImportBackupModal />
{/* eslint-disable-next-line */} {/* eslint-disable-next-line */}
<a id='downloadAnchor' target='_blank' style={{ display: 'none' }} download /> <a id='downloadAnchor' target='_blank' style={{ display: 'none' }} download />
</Layout.Content> </Layout.Content>

@ -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<number | null>(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 (
<Modal
title={<Text strong>{`Export ${instanceType}`}</Text>}
visible={!!instance}
onCancel={closeModal}
onOk={() => form.submit()}
className={`cvat-modal-export-${instanceType.split(' ')[0]}`}
destroyOnClose
>
<Form
name={`Export ${instanceType}`}
form={form}
layout='vertical'
initialValues={initialValues}
onFinish={handleExport}
>
<Form.Item label={<Text strong>Custom name</Text>} name='customName'>
<Input
placeholder='Custom name for a backup file'
suffix='.zip'
className='cvat-modal-export-filename-input'
/>
</Form.Item>
<TargetStorageField
instanceId={instance?.id}
switchDescription='Use default settings'
switchHelpMessage={helpMessage}
useDefaultStorage={useDefaultStorage}
storageDescription={`Specify target storage for export ${instanceType}`}
locationValue={storageLocation}
onChangeUseDefaultStorage={(value: boolean) => setUseDefaultStorage(value)}
onChangeLocationValue={(value: StorageLocation) => setStorageLocation(value)}
/>
</Form>
</Modal>
);
}
export default React.memo(ExportBackupModal);

@ -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;
}
}

@ -1,22 +1,24 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { connect, useDispatch } from 'react-redux';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import Notification from 'antd/lib/notification'; import Notification from 'antd/lib/notification';
import { useSelector, useDispatch } from 'react-redux';
import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import Select from 'antd/lib/select'; import Select from 'antd/lib/select';
import Checkbox from 'antd/lib/checkbox';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import Form from 'antd/lib/form'; import Form from 'antd/lib/form';
import Switch from 'antd/lib/switch';
import { CombinedState } from 'reducers'; 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 { exportActions, exportDatasetAsync } from 'actions/export-actions';
import { getCore } from 'cvat-core-wrapper'; import { getCore, Storage, StorageData } from 'cvat-core-wrapper';
const core = getCore(); const core = getCore();
@ -24,43 +26,94 @@ type FormValues = {
selectedFormat: string | undefined; selectedFormat: string | undefined;
saveImages: boolean; saveImages: boolean;
customName: string | undefined; 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 [instanceType, setInstanceType] = useState('');
const [activities, setActivities] = useState<string[]>([]);
const [useDefaultTargetStorage, setUseDefaultTargetStorage] = useState(true);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [targetStorage, setTargetStorage] = useState<StorageData>({
location: StorageLocation.LOCAL,
});
const [defaultStorageLocation, setDefaultStorageLocation] = useState(StorageLocation.LOCAL);
const [defaultStorageCloudId, setDefaultStorageCloudId] = useState<number | null>(null);
const [helpMessage, setHelpMessage] = useState('');
const dispatch = useDispatch(); 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) { if (instance instanceof core.classes.Project) {
setInstanceType(`project #${instance.id}`); setInstanceType(`project #${instance.id}`);
setActivities(projectExportActivities[instance.id] || []); } else if (instance instanceof core.classes.Task || instance instanceof core.classes.Job) {
} else if (instance) { if (instance instanceof core.classes.Task) {
const taskID = instance instanceof core.classes.Task ? instance.id : instance.taskId; setInstanceType(`task #${instance.id}`);
setInstanceType(`task #${taskID}`); } else {
setActivities(taskExportActivities[taskID] || []); setInstanceType(`job #${instance.id}`);
}
if (instance.mode === 'interpolation' && instance.dimension === '2d') { if (instance.mode === 'interpolation' && instance.dimension === '2d') {
form.setFieldsValue({ selectedFormat: 'CVAT for video 1.1' }); form.setFieldsValue({ selectedFormat: 'CVAT for video 1.1' });
} else if (instance.mode === 'annotation' && instance.dimension === '2d') { } else if (instance.mode === 'annotation' && instance.dimension === '2d') {
form.setFieldsValue({ selectedFormat: 'CVAT for images 1.1' }); form.setFieldsValue({ selectedFormat: 'CVAT for images 1.1' });
} }
} }
}; }, [instance]);
useEffect(() => { useEffect(() => {
initActivities(); if (instance) {
}, [instance?.id, instance instanceof core.classes.Project, taskExportActivities, projectExportActivities]); 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 => { const closeModal = (): void => {
setUseDefaultTargetStorage(true);
setTargetStorage({ location: StorageLocation.LOCAL });
form.resetFields(); form.resetFields();
dispatch(exportActions.closeExportModal()); dispatch(exportActions.closeExportDatasetModal(instance));
}; };
const handleExport = useCallback( const handleExport = useCallback(
@ -70,26 +123,32 @@ function ExportDatasetModal(): JSX.Element {
exportDatasetAsync( exportDatasetAsync(
instance, instance,
values.selectedFormat as string, values.selectedFormat as string,
values.customName ? `${values.customName}.zip` : '',
values.saveImages, values.saveImages,
useDefaultTargetStorage,
useDefaultTargetStorage ? new Storage({
location: defaultStorageLocation,
cloudStorageId: defaultStorageCloudId,
}) : new Storage(targetStorage),
values.customName ? `${values.customName}.zip` : null,
), ),
); );
closeModal(); closeModal();
const resource = values.saveImages ? 'Dataset' : 'Annotations';
Notification.info({ Notification.info({
message: 'Dataset export started', message: `${resource} export started`,
description: description:
`Dataset export was started for ${instanceType}. ` + `${resource} export was started for ${instanceType}. ` +
'Download will start automatically as soon as the dataset is ready.', `Download will start automatically as soon as the ${resource} is ready.`,
className: `cvat-notification-notice-export-${instanceType.split(' ')[0]}-start`, className: `cvat-notification-notice-export-${instanceType.split(' ')[0]}-start`,
}); });
}, },
[instance, instanceType], [instance, instanceType, useDefaultTargetStorage, defaultStorageLocation, defaultStorageCloudId, targetStorage],
); );
return ( return (
<Modal <Modal
title={`Export ${instanceType} as a dataset`} title={<Text strong>{`Export ${instanceType} as a dataset`}</Text>}
visible={modalVisible} visible={!!instance}
onCancel={closeModal} onCancel={closeModal}
onOk={() => form.submit()} onOk={() => form.submit()}
className={`cvat-modal-export-${instanceType.split(' ')[0]}`} className={`cvat-modal-export-${instanceType.split(' ')[0]}`}
@ -98,20 +157,13 @@ function ExportDatasetModal(): JSX.Element {
<Form <Form
name='Export dataset' name='Export dataset'
form={form} form={form}
labelCol={{ span: 8 }} layout='vertical'
wrapperCol={{ span: 16 }} initialValues={initialValues}
initialValues={
{
selectedFormat: undefined,
saveImages: false,
customName: undefined,
} as FormValues
}
onFinish={handleExport} onFinish={handleExport}
> >
<Form.Item <Form.Item
name='selectedFormat' name='selectedFormat'
label='Export format' label={<Text strong>Export format</Text>}
rules={[{ required: true, message: 'Format must be selected' }]} rules={[{ required: true, message: 'Format must be selected' }]}
> >
<Select virtual={false} placeholder='Select dataset format' className='cvat-modal-export-select'> <Select virtual={false} placeholder='Select dataset format' className='cvat-modal-export-select'>
@ -120,7 +172,8 @@ function ExportDatasetModal(): JSX.Element {
.filter((dumper: any): boolean => dumper.dimension === instance?.dimension) .filter((dumper: any): boolean => dumper.dimension === instance?.dimension)
.map( .map(
(dumper: any): JSX.Element => { (dumper: any): JSX.Element => {
const pending = (activities || []).includes(dumper.name); const pending = (instance && current ? current : [])
.includes(dumper.name);
const disabled = !dumper.enabled || pending; const disabled = !dumper.enabled || pending;
return ( return (
<Select.Option <Select.Option
@ -138,19 +191,57 @@ function ExportDatasetModal(): JSX.Element {
)} )}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item name='saveImages' valuePropName='checked' wrapperCol={{ offset: 8, span: 16 }}> <Space>
<Checkbox>Save images</Checkbox> <Form.Item name='saveImages' className='cvat-modal-export-switch-use-default-storage'>
</Form.Item> <Switch className='cvat-modal-export-save-images' />
<Form.Item label='Custom name' name='customName'> </Form.Item>
<Text strong>Save images</Text>
</Space>
<Form.Item label={<Text strong>Custom name</Text>} name='customName'>
<Input <Input
placeholder='Custom name for a dataset' placeholder='Custom name for a dataset'
suffix='.zip' suffix='.zip'
className='cvat-modal-export-filename-input' className='cvat-modal-export-filename-input'
/> />
</Form.Item> </Form.Item>
<TargetStorageField
instanceId={instance?.id}
switchDescription='Use default settings'
switchHelpMessage={helpMessage}
useDefaultStorage={useDefaultTargetStorage}
storageDescription='Specify target storage for export dataset'
locationValue={targetStorage.location}
onChangeUseDefaultStorage={(value: boolean) => setUseDefaultTargetStorage(value)}
onChangeStorage={(value: StorageData) => setTargetStorage(value)}
onChangeLocationValue={(value: StorageLocation) => {
setTargetStorage({ location: value });
}}
/>
</Form> </Form>
</Modal> </Modal>
); );
} }
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);

@ -11,3 +11,7 @@
margin-right: $grid-unit-size; margin-right: $grid-unit-size;
} }
} }
.cvat-modal-export-switch-use-default-storage {
display: table-cell;
}

@ -1,20 +1,14 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Form from 'antd/lib/form'; 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 Select from 'antd/lib/select';
import { getCore } from 'cvat-core-wrapper';
import { CloudStorage } from 'reducers'; import { CloudStorage } from 'reducers';
import { AzureProvider, GoogleCloudProvider, S3Provider } from 'icons'; import SelectCloudStorage from 'components/select-cloud-storage/select-cloud-storage';
import { ProviderType } from 'utils/enums';
import CloudStorageFiles from './cloud-storages-files'; import CloudStorageFiles from './cloud-storages-files';
interface Props { interface Props {
@ -27,61 +21,15 @@ interface Props {
onSelectCloudStorage: (cloudStorageId: number | null) => void; onSelectCloudStorage: (cloudStorageId: number | null) => void;
} }
async function searchCloudStorages(filter: Record<string, string>): Promise<CloudStorage[]> {
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 { 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 { export default function CloudStorageTab(props: Props): JSX.Element {
const { searchPhrase, setSearchPhrase } = props; const { searchPhrase, setSearchPhrase } = props;
const [initialList, setInitialList] = useState<CloudStorage[]>([]);
const [list, setList] = useState<CloudStorage[]>([]);
const { const {
formRef, cloudStorage, selectedFiles, onSelectFiles, onSelectCloudStorage, formRef, cloudStorage, selectedFiles, onSelectFiles, onSelectCloudStorage,
} = props; } = props;
const [selectedManifest, setSelectedManifest] = useState<string | null>(null); const [selectedManifest, setSelectedManifest] = useState<string | null>(null);
useEffect(() => {
searchCloudStorages({}).then((data) => {
setInitialList(data);
if (!list.length) {
setList(data);
}
});
}, []);
useEffect(() => {
if (!searchPhrase) {
setList(initialList);
} else {
searchCloudStoragesWrapper(searchPhrase, setList);
}
}, [searchPhrase, initialList]);
useEffect(() => { useEffect(() => {
if (cloudStorage) { if (cloudStorage) {
setSelectedManifest(cloudStorage.manifests[0]); setSelectedManifest(cloudStorage.manifests[0]);
@ -94,67 +42,15 @@ export default function CloudStorageTab(props: Props): JSX.Element {
} }
}, [selectedManifest]); }, [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 ( return (
<Form ref={formRef} className='cvat-create-task-page-cloud-storages-tab-form' layout='vertical'> <Form ref={formRef} className='cvat-create-task-page-cloud-storages-tab-form' layout='vertical'>
<Form.Item
label='Select cloud storage'
name='cloudStorageSelect'
rules={[{ required: true, message: 'Please, specify a cloud storage' }]}
valuePropName='label'
>
<AutoComplete
onBlur={onBlur}
value={searchPhrase}
placeholder='Search...'
showSearch
onSearch={(phrase: string) => {
setSearchPhrase(phrase);
}}
options={list.map((_cloudStorage) => ({
value: _cloudStorage.id.toString(),
label: (
<span
className='cvat-cloud-storage-select-provider'
>
{_cloudStorage.providerType === ProviderType.AWS_S3_BUCKET && <S3Provider />}
{_cloudStorage.providerType === ProviderType.AZURE_CONTAINER && <AzureProvider />}
{
_cloudStorage.providerType === ProviderType.GOOGLE_CLOUD_STORAGE &&
<GoogleCloudProvider />
}
{_cloudStorage.displayName}
</span>
),
}))}
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
>
<Input />
</AutoComplete>
</Form.Item>
<SelectCloudStorage
searchPhrase={searchPhrase}
cloudStorage={cloudStorage}
setSearchPhrase={setSearchPhrase}
onSelectCloudStorage={onSelectCloudStorage}
/>
{cloudStorage ? ( {cloudStorage ? (
<Form.Item <Form.Item
label='Select manifest file' label='Select manifest file'

@ -0,0 +1,172 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Modal from 'antd/lib/modal';
import Form, { RuleObject } from 'antd/lib/form';
import Text from 'antd/lib/typography/Text';
import Notification from 'antd/lib/notification';
import message from 'antd/lib/message';
import Upload, { RcFile } from 'antd/lib/upload';
import { InboxOutlined } from '@ant-design/icons';
import { CombinedState, StorageLocation } from 'reducers';
import { importActions, importBackupAsync } from 'actions/import-actions';
import SourceStorageField from 'components/storage/source-storage-field';
import Input from 'antd/lib/input/Input';
import { Storage, StorageData } from 'cvat-core-wrapper';
type FormValues = {
fileName?: string | undefined;
sourceStorage: StorageData;
};
const initialValues: FormValues = {
fileName: undefined,
sourceStorage: {
location: StorageLocation.LOCAL,
cloudStorageId: undefined,
},
};
function ImportBackupModal(): JSX.Element {
const [form] = Form.useForm();
const [file, setFile] = useState<File | null>(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<StorageData>({
location: StorageLocation.LOCAL,
});
const uploadLocalFile = (): JSX.Element => (
<Upload.Dragger
listType='text'
fileList={file ? [file] : ([] as any[])}
beforeUpload={(_file: RcFile): boolean => {
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);
}}
>
<p className='ant-upload-drag-icon'>
<InboxOutlined />
</p>
<p className='ant-upload-text'>Click or drag file to this area</p>
</Upload.Dragger>
);
const validateFileName = (_: RuleObject, value: string): Promise<void> => {
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 => (
<Form.Item
label={<Text strong>File name</Text>}
name='fileName'
rules={[{ validator: validateFileName }]}
>
<Input
placeholder='Backup file name'
className='cvat-modal-import-filename-input'
/>
</Form.Item>
);
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 (
<>
<Modal
title={(
<Text strong>
Create
{instanceType}
{' '}
from backup
</Text>
)}
visible={modalVisible}
onCancel={closeModal}
onOk={() => form.submit()}
className='cvat-modal-import-backup'
>
<Form
name={`Create ${instanceType} from backup file`}
form={form}
onFinish={handleImport}
layout='vertical'
initialValues={initialValues}
>
<SourceStorageField
instanceId={null}
storageDescription='Specify source storage with backup'
locationValue={selectedSourceStorage.location}
onChangeStorage={(value: StorageData) => setSelectedSourceStorage(new Storage(value))}
onChangeLocationValue={(value: StorageLocation) => {
setSelectedSourceStorage({
location: value,
});
}}
/>
{selectedSourceStorage?.location === StorageLocation.CLOUD_STORAGE && renderCustomName()}
{selectedSourceStorage?.location === StorageLocation.LOCAL && uploadLocalFile()}
</Form>
</Modal>
</>
);
}
export default React.memo(ImportBackupModal);

@ -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<File | null>(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 (
<>
<Modal
title={(
<>
<Text>Import dataset to project</Text>
<CVATTooltip
title={
instance && !instance.labels.length ?
'Labels will be imported from dataset' :
'Labels from project will be used'
}
>
<QuestionCircleOutlined className='cvat-modal-import-header-question-icon' />
</CVATTooltip>
</>
)}
visible={modalVisible}
onCancel={closeModal}
onOk={() => form.submit()}
className='cvat-modal-import-dataset'
>
<Form
name='Import dataset'
form={form}
initialValues={{ selectedFormat: undefined } as FormValues}
onFinish={handleImport}
>
<Form.Item
name='selectedFormat'
label='Import format'
rules={[{ required: true, message: 'Format must be selected' }]}
>
<Select placeholder='Select dataset format' className='cvat-modal-import-select'>
{importers
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter(
(importer: any): boolean => (
instance !== null &&
(!instance?.dimension || importer.dimension === instance.dimension)
),
)
.map(
(importer: any): JSX.Element => {
const pending = currentImportId !== null;
const disabled = !importer.enabled || pending;
return (
<Select.Option
value={importer.name}
key={importer.name}
disabled={disabled}
className='cvat-modal-import-dataset-option-item'
>
<UploadOutlined />
<Text disabled={disabled}>{importer.name}</Text>
{pending && <LoadingOutlined style={{ marginLeft: 10 }} />}
</Select.Option>
);
},
)}
</Select>
</Form.Item>
<Upload.Dragger
listType='text'
fileList={file ? [file] : ([] as any[])}
beforeUpload={(_file: RcFile): boolean => {
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);
}}
>
<p className='ant-upload-drag-icon'>
<InboxOutlined />
</p>
<p className='ant-upload-text'>Click or drag file to this area</p>
</Upload.Dragger>
</Form>
</Modal>
<ImportDatasetStatusModal />
</>
);
}
export default React.memo(ImportDatasetModal);

@ -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 (
<Modal
title={`Importing a dataset for the project #${currentImportId}`}
visible={currentImportId !== null}
closable={false}
footer={null}
className='cvat-modal-import-dataset-status'
destroyOnClose
>
<Progress type='circle' percent={progress} />
<Alert message={status} type='info' />
</Modal>
);
}
export default React.memo(ImportDatasetStatusModal);

@ -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<File | null>(null);
const [selectedLoader, setSelectedLoader] = useState<any>(null);
const [useDefaultSettings, setUseDefaultSettings] = useState(true);
const [defaultStorageLocation, setDefaultStorageLocation] = useState(StorageLocation.LOCAL);
const [defaultStorageCloudId, setDefaultStorageCloudId] = useState<number | undefined>(undefined);
const [helpMessage, setHelpMessage] = useState('');
const [selectedSourceStorageLocation, setSelectedSourceStorageLocation] = useState(StorageLocation.LOCAL);
const [uploadParams, setUploadParams] = useState<UploadParams>({
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 => (
<Upload.Dragger
listType='text'
fileList={file ? [file] : ([] as any[])}
accept='.zip,.json,.xml'
beforeUpload={(_file: RcFile): boolean => {
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);
}}
>
<p className='ant-upload-drag-icon'>
<InboxOutlined />
</p>
<p className='ant-upload-text'>Click or drag file to this area</p>
</Upload.Dragger>
);
const validateFileName = (_: RuleObject, value: string): Promise<void> => {
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 => (
<Form.Item
label={<Text strong>File name</Text>}
name='fileName'
hasFeedback
dependencies={['selectedFormat']}
rules={[{ validator: validateFileName }]}
required
>
<Input
placeholder='Dataset file name'
className='cvat-modal-import-filename-input'
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value) {
setUploadParams({
...uploadParams,
fileName: e.target.value,
} as UploadParams);
}
}}
/>
</Form.Item>
);
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 (
<>
<Modal
title={(
<>
<Text strong>
{`Import ${resource} to ${instanceType}`}
</Text>
{
instance instanceof core.classes.Project && (
<CVATTooltip
title={
instance && !instance.labels.length ?
'Labels will be imported from dataset' :
'Labels from project will be used'
}
>
<QuestionCircleOutlined className='cvat-modal-import-header-question-icon' />
</CVATTooltip>
)
}
</>
)}
visible={!!instance}
onCancel={closeModal}
onOk={() => form.submit()}
className='cvat-modal-import-dataset'
>
<Form
name={`Import ${resource}`}
form={form}
initialValues={initialValues}
onFinish={handleImport}
layout='vertical'
>
<Form.Item
name='selectedFormat'
label='Import format'
rules={[{ required: true, message: 'Format must be selected' }]}
hasFeedback
>
<Select
placeholder={`Select ${resource} format`}
className='cvat-modal-import-select'
virtual={false}
onChange={(format: string) => {
const [loader] = importers.filter(
(importer: any): boolean => importer.name === format,
);
setSelectedLoader(loader);
setUploadParams({
...uploadParams,
selectedFormat: format,
} as UploadParams);
}}
>
{importers
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter(
(importer: any): boolean => (
instance !== null &&
(!instance?.dimension || importer.dimension === instance.dimension)
),
)
.map(
(importer: any): JSX.Element => {
const pending = current ? instance.id in current : false;
const disabled = !importer.enabled || pending;
return (
<Select.Option
value={importer.name}
key={importer.name}
disabled={disabled}
className='cvat-modal-import-dataset-option-item'
>
<UploadOutlined />
<Text disabled={disabled}>{importer.name}</Text>
{pending && <LoadingOutlined style={{ marginLeft: 10 }} />}
</Select.Option>
);
},
)}
</Select>
</Form.Item>
<Space>
<Form.Item
name='useDefaultSettings'
valuePropName='checked'
className='cvat-modal-import-switch-use-default-storage'
>
<Switch
onChange={(value: boolean) => {
setUseDefaultSettings(value);
setUploadParams({
...uploadParams,
useDefaultSettings: value,
} as UploadParams);
}}
/>
</Form.Item>
<Text strong>Use default settings</Text>
<CVATTooltip title={helpMessage}>
<QuestionCircleOutlined />
</CVATTooltip>
</Space>
{
useDefaultSettings && (
defaultStorageLocation === StorageLocation.LOCAL ||
defaultStorageLocation === null
) && uploadLocalFile()
}
{
useDefaultSettings &&
defaultStorageLocation === StorageLocation.CLOUD_STORAGE &&
renderCustomName()
}
{!useDefaultSettings && (
<StorageField
locationName={['sourceStorage', 'location']}
selectCloudStorageName={['sourceStorage', 'cloudStorageId']}
onChangeStorage={(value: StorageData) => {
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()
}
</Form>
</Modal>
<ImportDatasetStatusModal />
</>
);
}
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);

@ -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<number | null>(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 (
<Modal
title={`Importing a dataset for the project #${importingId}`}
visible={importing}
closable={false}
footer={null}
className='cvat-modal-import-dataset-status'
destroyOnClose
>
<Progress type='circle' percent={progress} />
<Alert message={status} type='info' />
</Modal>
);
}
export default React.memo(ImportDatasetStatusModal);

@ -17,6 +17,10 @@
color: $text-color-secondary; color: $text-color-secondary;
} }
.cvat-modal-import-switch-use-default-storage {
display: table-cell;
}
.cvat-modal-import-dataset-status .ant-modal-body { .cvat-modal-import-dataset-status .ant-modal-body {
display: flex; display: flex;
align-items: center; align-items: center;

@ -1,8 +1,10 @@
// Copyright (C) 2022 Intel Corporation // Copyright (C) 2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import Card from 'antd/lib/card'; import Card from 'antd/lib/card';
import Empty from 'antd/lib/empty'; import Empty from 'antd/lib/empty';
@ -12,8 +14,8 @@ import Dropdown from 'antd/lib/dropdown';
import Menu from 'antd/lib/menu'; import Menu from 'antd/lib/menu';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface'; import { MenuInfo } from 'rc-menu/lib/interface';
import { useCardHeightHOC } from 'utils/hooks'; import { useCardHeightHOC } from 'utils/hooks';
import { exportActions } from 'actions/export-actions';
const useCardHeight = useCardHeightHOC({ const useCardHeight = useCardHeightHOC({
containerClassName: 'cvat-jobs-page', containerClassName: 'cvat-jobs-page',
@ -28,6 +30,7 @@ interface Props {
} }
function JobCardComponent(props: Props): JSX.Element { function JobCardComponent(props: Props): JSX.Element {
const dispatch = useDispatch();
const { job, preview } = props; const { job, preview } = props;
const [expanded, setExpanded] = useState<boolean>(false); const [expanded, setExpanded] = useState<boolean>(false);
const history = useHistory(); const history = useHistory();
@ -97,6 +100,7 @@ function JobCardComponent(props: Props): JSX.Element {
<Menu.Item key='task' disabled={job.taskId === null}>Go to the task</Menu.Item> <Menu.Item key='task' disabled={job.taskId === null}>Go to the task</Menu.Item>
<Menu.Item key='project' disabled={job.projectId === null}>Go to the project</Menu.Item> <Menu.Item key='project' disabled={job.projectId === null}>Go to the project</Menu.Item>
<Menu.Item key='bug_tracker' disabled={!job.bugTracker}>Go to the bug tracker</Menu.Item> <Menu.Item key='bug_tracker' disabled={!job.bugTracker}>Go to the bug tracker</Menu.Item>
<Menu.Item key='export_job' onClick={() => dispatch(exportActions.openExportDatasetModal(job))}>Export job</Menu.Item>
</Menu> </Menu>
)} )}
> >

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -22,7 +23,6 @@ import { cancelInferenceAsync } from 'actions/models-actions';
import TaskItem from 'components/tasks-page/task-item'; import TaskItem from 'components/tasks-page/task-item';
import MoveTaskModal from 'components/move-task-modal/move-task-modal'; import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog'; import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog';
import ImportDatasetModal from 'components/import-dataset-modal/import-dataset-modal';
import { import {
SortingComponent, ResourceFilterHOC, defaultVisibility, updateHistoryFromQuery, SortingComponent, ResourceFilterHOC, defaultVisibility, updateHistoryFromQuery,
} from 'components/resource-sorting-filtering'; } from 'components/resource-sorting-filtering';
@ -241,7 +241,6 @@ export default function ProjectPageComponent(): JSX.Element {
<MoveTaskModal /> <MoveTaskModal />
<ModelRunnerDialog /> <ModelRunnerDialog />
<ImportDatasetModal />
</Row> </Row>
); );
} }

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -7,9 +8,8 @@ import { useDispatch, useSelector } from 'react-redux';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import Menu from 'antd/lib/menu'; import Menu from 'antd/lib/menu';
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
import { CombinedState } from 'reducers'; import { CombinedState } from 'reducers';
import { deleteProjectAsync, backupProjectAsync } from 'actions/projects-actions'; import { deleteProjectAsync } from 'actions/projects-actions';
import { exportActions } from 'actions/export-actions'; import { exportActions } from 'actions/export-actions';
import { importActions } from 'actions/import-actions'; import { importActions } from 'actions/import-actions';
@ -21,8 +21,9 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element {
const { projectInstance } = props; const { projectInstance } = props;
const dispatch = useDispatch(); const dispatch = useDispatch();
const activeBackups = useSelector((state: CombinedState) => state.projects.activities.backups); const exportBackupIsActive = useSelector((state: CombinedState) => (
const exportIsActive = projectInstance.id in activeBackups; state.export.projects.backup.current[projectInstance.id]
));
const onDeleteProject = useCallback((): void => { const onDeleteProject = useCallback((): void => {
Modal.confirm({ Modal.confirm({
@ -42,16 +43,16 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element {
return ( return (
<Menu selectable={false} className='cvat-project-actions-menu'> <Menu selectable={false} className='cvat-project-actions-menu'>
<Menu.Item key='export-dataset' onClick={() => dispatch(exportActions.openExportModal(projectInstance))}> <Menu.Item key='export-dataset' onClick={() => dispatch(exportActions.openExportDatasetModal(projectInstance))}>
Export dataset Export dataset
</Menu.Item> </Menu.Item>
<Menu.Item key='import-dataset' onClick={() => dispatch(importActions.openImportModal(projectInstance))}> <Menu.Item key='import-dataset' onClick={() => dispatch(importActions.openImportDatasetModal(projectInstance))}>
Import dataset Import dataset
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
disabled={exportIsActive} disabled={exportBackupIsActive}
onClick={() => dispatch(backupProjectAsync(projectInstance))} onClick={() => dispatch(exportActions.openExportBackupModal(projectInstance))}
icon={exportIsActive && <LoadingOutlined id='cvat-export-project-loading' />} icon={exportBackupIsActive && <LoadingOutlined id='cvat-export-project-loading' />}
> >
Backup Project Backup Project
</Menu.Item> </Menu.Item>

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -7,12 +8,10 @@ import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import Spin from 'antd/lib/spin'; import Spin from 'antd/lib/spin';
import { CombinedState, Indexable } from 'reducers'; 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 FeedbackComponent from 'components/feedback/feedback';
import { updateHistoryFromQuery } from 'components/resource-sorting-filtering'; import { updateHistoryFromQuery } from 'components/resource-sorting-filtering';
import ImportDatasetModal from 'components/import-dataset-modal/import-dataset-modal';
import EmptyListComponent from './empty-list'; import EmptyListComponent from './empty-list';
import TopBarComponent from './top-bar'; import TopBarComponent from './top-bar';
import ProjectListComponent from './project-list'; 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 count = useSelector((state: CombinedState) => state.projects.current.length);
const query = useSelector((state: CombinedState) => state.projects.gettingQuery); const query = useSelector((state: CombinedState) => state.projects.gettingQuery);
const tasksQuery = useSelector((state: CombinedState) => state.projects.tasksGettingQuery); 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 [isMounted, setIsMounted] = useState(false);
const anySearch = Object.keys(query).some((value: string) => value !== 'page' && (query as any)[value] !== null); 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} query={updatedQuery}
onImportProject={(file: File) => dispatch(restoreProjectAsync(file))}
importing={importing} importing={importing}
/> />
{ fetching ? ( { fetching ? (
@ -92,7 +90,6 @@ export default function ProjectsPageComponent(): JSX.Element {
</div> </div>
) : content } ) : content }
<FeedbackComponent /> <FeedbackComponent />
<ImportDatasetModal />
</div> </div>
); );
} }

@ -1,16 +1,17 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useDispatch } from 'react-redux';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Dropdown from 'antd/lib/dropdown'; import Dropdown from 'antd/lib/dropdown';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons'; 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 { usePrevious } from 'utils/hooks';
import { ProjectsQuery } from 'reducers'; import { ProjectsQuery } from 'reducers';
import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering'; import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering';
@ -24,7 +25,6 @@ const FilteringComponent = ResourceFilterHOC(
); );
interface Props { interface Props {
onImportProject(file: File): void;
onApplyFilter(filter: string | null): void; onApplyFilter(filter: string | null): void;
onApplySorting(sorting: string | null): void; onApplySorting(sorting: string | null): void;
onApplySearch(search: string | null): void; onApplySearch(search: string | null): void;
@ -33,8 +33,9 @@ interface Props {
} }
function TopBarComponent(props: Props): JSX.Element { function TopBarComponent(props: Props): JSX.Element {
const dispatch = useDispatch();
const { const {
importing, query, onApplyFilter, onApplySorting, onApplySearch, onImportProject, importing, query, onApplyFilter, onApplySorting, onApplySearch,
} = props; } = props;
const [visibility, setVisibility] = useState(defaultVisibility); const [visibility, setVisibility] = useState(defaultVisibility);
const prevImporting = usePrevious(importing); const prevImporting = usePrevious(importing);
@ -101,26 +102,16 @@ function TopBarComponent(props: Props): JSX.Element {
> >
Create a new project Create a new project
</Button> </Button>
<Upload <Button
accept='.zip' className='cvat-import-project-button'
multiple={false} type='primary'
showUploadList={false} disabled={importing}
beforeUpload={(file: File): boolean => { icon={<UploadOutlined />}
onImportProject(file); onClick={() => dispatch(importActions.openImportBackupModal('project'))}
return false;
}}
className='cvat-import-project'
> >
<Button Create from backup
className='cvat-import-project-button' {importing && <LoadingOutlined className='cvat-import-project-button-loading' />}
type='primary' </Button>
disabled={importing}
icon={<UploadOutlined />}
>
Create from backup
{importing && <LoadingOutlined className='cvat-import-project-button-loading' />}
</Button>
</Upload>
</div> </div>
)} )}
> >

@ -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<string, string>): Promise<CloudStorage[]> {
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<CloudStorage[]>([]);
const [list, setList] = useState<CloudStorage[]>([]);
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 (
<Form.Item
label='Select cloud storage'
name={name || 'cloudStorageSelect'}
rules={[{ required: true, message: 'Please, specify a cloud storage' }]}
valuePropName='label'
>
<AutoComplete
onBlur={onBlur}
value={searchPhrase}
placeholder='Search...'
showSearch
onSearch={(phrase: string) => {
setSearchPhrase(phrase);
}}
options={list.map((_cloudStorage) => ({
value: _cloudStorage.id.toString(),
label: (
<span
className='cvat-cloud-storage-select-provider'
>
{_cloudStorage.providerType === ProviderType.AWS_S3_BUCKET && <S3Provider />}
{_cloudStorage.providerType === ProviderType.AZURE_CONTAINER && <AzureProvider />}
{
_cloudStorage.providerType === ProviderType.GOOGLE_CLOUD_STORAGE &&
<GoogleCloudProvider />
}
{_cloudStorage.displayName}
</span>
),
}))}
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
>
<Input />
</AutoComplete>
</Form.Item>
);
}
export default React.memo(SelectCloudStorage);

@ -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 (
<StorageWithSwitchField
storageLabel='Source storage'
storageName='sourceStorage'
switchName='useProjectSourceStorage'
instanceId={instanceId}
locationValue={locationValue}
useDefaultStorage={useDefaultStorage}
switchDescription={switchDescription}
switchHelpMessage={switchHelpMessage}
storageDescription={storageDescription}
onChangeUseDefaultStorage={onChangeUseDefaultStorage}
onChangeStorage={onChangeStorage}
onChangeLocationValue={onChangeLocationValue}
/>
);
}

@ -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<CloudStorage | null>(null);
const [potentialCloudStorage, setPotentialCloudStorage] = useState('');
function renderCloudStorage(): JSX.Element {
return (
<SelectCloudStorage
searchPhrase={potentialCloudStorage}
cloudStorage={cloudStorage}
setSearchPhrase={(cs: string) => {
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 (
<>
<Form.Item name={locationName}>
<Select
onChange={(location: StorageLocation) => {
if (onChangeLocationValue) onChangeLocationValue(location);
}}
onClear={() => {
if (onChangeLocationValue) onChangeLocationValue(StorageLocation.LOCAL);
}}
allowClear
>
<Option value={StorageLocation.LOCAL}>Local</Option>
<Option value={StorageLocation.CLOUD_STORAGE}>Cloud storage</Option>
</Select>
</Form.Item>
{locationValue === StorageLocation.CLOUD_STORAGE && renderCloudStorage()}
</>
);
}

@ -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 && (
<Space>
<Form.Item
name={switchName}
valuePropName='checked'
className='cvat-settings-switch'
>
<Switch
onChange={(value: boolean) => {
if (onChangeUseDefaultStorage) {
onChangeUseDefaultStorage(value);
}
}}
/>
</Form.Item>
<Text strong>{switchDescription}</Text>
{(switchHelpMessage) ? (
<Tooltip title={switchHelpMessage}>
<QuestionCircleOutlined />
</Tooltip>
) : null}
</Space>
)
}
{
(!instanceId || !useDefaultStorage) && (
<Form.Item
label={(
<>
<Space>
{storageLabel}
<CVATTooltip title={storageDescription}>
<QuestionCircleOutlined
style={{ opacity: 0.5 }}
/>
</CVATTooltip>
</Space>
</>
)}
>
<StorageField
locationName={[storageName, 'location']}
selectCloudStorageName={[storageName, 'cloudStorageId']}
locationValue={locationValue}
onChangeStorage={onChangeStorage}
onChangeLocationValue={onChangeLocationValue}
/>
</Form.Item>
)
}
</>
);
}

@ -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;
}

@ -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 (
<StorageWithSwitchField
instanceId={instanceId}
locationValue={locationValue}
storageLabel='Target storage'
storageName='targetStorage'
switchName='useProjectTargetStorage'
useDefaultStorage={useDefaultStorage}
switchDescription={switchDescription}
switchHelpMessage={switchHelpMessage}
storageDescription={storageDescription}
onChangeUseDefaultStorage={onChangeUseDefaultStorage}
onChangeStorage={onChangeStorage}
onChangeLocationValue={onChangeLocationValue}
/>
);
}

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -17,7 +18,7 @@ import { TasksQuery, Indexable } from 'reducers';
import FeedbackComponent from 'components/feedback/feedback'; import FeedbackComponent from 'components/feedback/feedback';
import { updateHistoryFromQuery } from 'components/resource-sorting-filtering'; import { updateHistoryFromQuery } from 'components/resource-sorting-filtering';
import TaskListContainer from 'containers/tasks-page/tasks-list'; 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 TopBar from './top-bar';
import EmptyListComponent from './empty-list'; import EmptyListComponent from './empty-list';
@ -139,7 +140,6 @@ function TasksPageComponent(props: Props): JSX.Element {
); );
}} }}
query={updatedQuery} query={updatedQuery}
onImportTask={(file: File) => dispatch(importTaskAsync(file))}
importing={importing} importing={importing}
/> />
{ fetching ? ( { fetching ? (

@ -1,16 +1,18 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Dropdown from 'antd/lib/dropdown'; import Dropdown from 'antd/lib/dropdown';
import { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons'; import { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Upload from 'antd/lib/upload';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import { importActions } from 'actions/import-actions';
import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering'; import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering';
import { TasksQuery } from 'reducers'; import { TasksQuery } from 'reducers';
import { usePrevious } from 'utils/hooks'; import { usePrevious } from 'utils/hooks';
@ -23,7 +25,6 @@ const FilteringComponent = ResourceFilterHOC(
); );
interface VisibleTopBarProps { interface VisibleTopBarProps {
onImportTask(file: File): void;
onApplyFilter(filter: string | null): void; onApplyFilter(filter: string | null): void;
onApplySorting(sorting: string | null): void; onApplySorting(sorting: string | null): void;
onApplySearch(search: string | null): void; onApplySearch(search: string | null): void;
@ -32,8 +33,9 @@ interface VisibleTopBarProps {
} }
export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element { export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element {
const dispatch = useDispatch();
const { const {
importing, query, onApplyFilter, onApplySorting, onApplySearch, onImportTask, importing, query, onApplyFilter, onApplySorting, onApplySearch,
} = props; } = props;
const [visibility, setVisibility] = useState(defaultVisibility); const [visibility, setVisibility] = useState(defaultVisibility);
const history = useHistory(); const history = useHistory();
@ -99,26 +101,16 @@ export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element
> >
Create a new task Create a new task
</Button> </Button>
<Upload <Button
accept='.zip' className='cvat-import-task-button'
multiple={false} type='primary'
showUploadList={false} disabled={importing}
beforeUpload={(file: File): boolean => { icon={<UploadOutlined />}
onImportTask(file); onClick={() => dispatch(importActions.openImportBackupModal('task'))}
return false;
}}
className='cvat-import-task'
> >
<Button Create from backup
className='cvat-import-task-button' {importing && <LoadingOutlined />}
type='primary' </Button>
disabled={importing}
icon={<UploadOutlined />}
>
Create from backup
{importing && <LoadingOutlined />}
</Button>
</Upload>
</div> </div>
)} )}
> >

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -6,18 +7,16 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface'; import { MenuInfo } from 'rc-menu/lib/interface';
import ActionsMenuComponent, { Actions } from 'components/actions-menu/actions-menu'; import ActionsMenuComponent, { Actions } from 'components/actions-menu/actions-menu';
import { CombinedState } from 'reducers'; import { CombinedState } from 'reducers';
import { modelsActions } from 'actions/models-actions'; import { modelsActions } from 'actions/models-actions';
import { import {
loadAnnotationsAsync,
deleteTaskAsync, deleteTaskAsync,
exportTaskAsync,
switchMoveTaskModalVisible, switchMoveTaskModalVisible,
} from 'actions/tasks-actions'; } from 'actions/tasks-actions';
import { exportActions } from 'actions/export-actions'; import { exportActions } from 'actions/export-actions';
import { importActions } from 'actions/import-actions';
interface OwnProps { interface OwnProps {
taskInstance: any; taskInstance: any;
@ -25,17 +24,15 @@ interface OwnProps {
interface StateToProps { interface StateToProps {
annotationFormats: any; annotationFormats: any;
loadActivity: string | null;
inferenceIsActive: boolean; inferenceIsActive: boolean;
exportIsActive: boolean; backupIsActive: boolean;
} }
interface DispatchToProps { interface DispatchToProps {
loadAnnotations: (taskInstance: any, loader: any, file: File) => void; showExportModal: (taskInstance: any, resource: 'dataset' | 'backup') => void;
showExportModal: (taskInstance: any) => void; showImportModal: (taskInstance: any) => void;
deleteTask: (taskInstance: any) => void;
openRunModelWindow: (taskInstance: any) => void; openRunModelWindow: (taskInstance: any) => void;
exportTask: (taskInstance: any) => void; deleteTask: (taskInstance: any) => void;
openMoveTaskToProjectWindow: (taskInstance: any) => void; openMoveTaskToProjectWindow: (taskInstance: any) => void;
} }
@ -46,26 +43,26 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { const {
formats: { annotationFormats }, formats: { annotationFormats },
tasks: {
activities: { loads, backups },
},
} = state; } = state;
return { return {
loadActivity: tid in loads ? loads[tid] : null,
annotationFormats, annotationFormats,
inferenceIsActive: tid in state.models.inferences, inferenceIsActive: tid in state.models.inferences,
exportIsActive: tid in backups, backupIsActive: state.export.tasks.backup.current[tid],
}; };
} }
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
loadAnnotations: (taskInstance: any, loader: any, file: File): void => { showExportModal: (taskInstance: any, resource: 'dataset' | 'backup'): void => {
dispatch(loadAnnotationsAsync(taskInstance, loader, file)); if (resource === 'dataset') {
dispatch(exportActions.openExportDatasetModal(taskInstance));
} else {
dispatch(exportActions.openExportBackupModal(taskInstance));
}
}, },
showExportModal: (taskInstance: any): void => { showImportModal: (taskInstance: any): void => {
dispatch(exportActions.openExportModal(taskInstance)); dispatch(importActions.openImportDatasetModal(taskInstance));
}, },
deleteTask: (taskInstance: any): void => { deleteTask: (taskInstance: any): void => {
dispatch(deleteTaskAsync(taskInstance)); dispatch(deleteTaskAsync(taskInstance));
@ -73,9 +70,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
openRunModelWindow: (taskInstance: any): void => { openRunModelWindow: (taskInstance: any): void => {
dispatch(modelsActions.showRunModelDialog(taskInstance)); dispatch(modelsActions.showRunModelDialog(taskInstance));
}, },
exportTask: (taskInstance: any): void => {
dispatch(exportTaskAsync(taskInstance));
},
openMoveTaskToProjectWindow: (taskId: number): void => { openMoveTaskToProjectWindow: (taskId: number): void => {
dispatch(switchMoveTaskModalVisible(true, taskId)); dispatch(switchMoveTaskModalVisible(true, taskId));
}, },
@ -86,21 +80,19 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps):
const { const {
taskInstance, taskInstance,
annotationFormats: { loaders, dumpers }, annotationFormats: { loaders, dumpers },
loadActivity,
inferenceIsActive, inferenceIsActive,
exportIsActive, backupIsActive,
loadAnnotations,
showExportModal, showExportModal,
showImportModal,
deleteTask, deleteTask,
openRunModelWindow, openRunModelWindow,
exportTask,
openMoveTaskToProjectWindow, openMoveTaskToProjectWindow,
} = props; } = props;
const onClickMenu = (params: MenuInfo): void => { const onClickMenu = (params: MenuInfo): void | JSX.Element => {
const [action] = params.keyPath; const [action] = params.keyPath;
if (action === Actions.EXPORT_TASK_DATASET) { if (action === Actions.EXPORT_TASK_DATASET) {
showExportModal(taskInstance); showExportModal(taskInstance, 'dataset');
} else if (action === Actions.DELETE_TASK) { } else if (action === Actions.DELETE_TASK) {
deleteTask(taskInstance); deleteTask(taskInstance);
} else if (action === Actions.OPEN_BUG_TRACKER) { } else if (action === Actions.OPEN_BUG_TRACKER) {
@ -108,17 +100,12 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps):
window.open(`${taskInstance.bugTracker}`, '_blank'); window.open(`${taskInstance.bugTracker}`, '_blank');
} else if (action === Actions.RUN_AUTO_ANNOTATION) { } else if (action === Actions.RUN_AUTO_ANNOTATION) {
openRunModelWindow(taskInstance); openRunModelWindow(taskInstance);
} else if (action === Actions.EXPORT_TASK) { } else if (action === Actions.BACKUP_TASK) {
exportTask(taskInstance); showExportModal(taskInstance, 'backup');
} else if (action === Actions.MOVE_TASK_TO_PROJECT) { } else if (action === Actions.MOVE_TASK_TO_PROJECT) {
openMoveTaskToProjectWindow(taskInstance.id); openMoveTaskToProjectWindow(taskInstance.id);
} } else if (action === Actions.LOAD_TASK_ANNO) {
}; showImportModal(taskInstance);
const onUploadAnnotations = (format: string, file: File): void => {
const [loader] = loaders.filter((_loader: any): boolean => _loader.name === format);
if (loader && file) {
loadAnnotations(taskInstance, loader, file);
} }
}; };
@ -129,12 +116,10 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps):
bugTracker={taskInstance.bugTracker} bugTracker={taskInstance.bugTracker}
loaders={loaders} loaders={loaders}
dumpers={dumpers} dumpers={dumpers}
loadActivity={loadActivity}
inferenceIsActive={inferenceIsActive} inferenceIsActive={inferenceIsActive}
onClickMenu={onClickMenu} onClickMenu={onClickMenu}
onUploadAnnotations={onUploadAnnotations}
taskDimension={taskInstance.dimension} taskDimension={taskInstance.dimension}
exportIsActive={exportIsActive} backupIsActive={backupIsActive}
/> />
); );
} }

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // 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 AnnotationMenuComponent, { Actions } from 'components/annotation-page/top-bar/annotation-menu';
import { updateJobAsync } from 'actions/tasks-actions'; import { updateJobAsync } from 'actions/tasks-actions';
import { import {
uploadJobAnnotationsAsync,
saveAnnotationsAsync, saveAnnotationsAsync,
setForceExitAnnotationFlag as setForceExitAnnotationFlagAction, setForceExitAnnotationFlag as setForceExitAnnotationFlagAction,
removeAnnotationsAsync as removeAnnotationsAsyncAction, removeAnnotationsAsync as removeAnnotationsAsyncAction,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import { exportActions } from 'actions/export-actions'; import { exportActions } from 'actions/export-actions';
import { importActions } from 'actions/import-actions';
import { getCore } from 'cvat-core-wrapper'; import { getCore } from 'cvat-core-wrapper';
const core = getCore(); const core = getCore();
interface StateToProps { interface StateToProps {
annotationFormats: any;
jobInstance: any; jobInstance: any;
stopFrame: number; stopFrame: number;
loadActivity: string | null;
} }
interface DispatchToProps { 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; removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly: boolean): void;
setForceExitAnnotationFlag(forceExit: boolean): void; setForceExitAnnotationFlag(forceExit: boolean): void;
saveAnnotations(jobInstance: any, afterSave?: () => void): void; saveAnnotations(jobInstance: any, afterSave?: () => void): void;
@ -41,36 +40,26 @@ interface DispatchToProps {
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
const { const {
annotation: { annotation: {
activities: { loads: jobLoads },
job: { job: {
instance: jobInstance, instance: jobInstance,
instance: { stopFrame }, instance: { stopFrame },
}, },
}, },
formats: { annotationFormats },
tasks: {
activities: { loads },
},
} = state; } = state;
const taskID = jobInstance.taskId;
const jobID = jobInstance.id;
return { return {
loadActivity: taskID in loads || jobID in jobLoads ? loads[taskID] || jobLoads[jobID] : null,
jobInstance, jobInstance,
stopFrame, stopFrame,
annotationFormats,
}; };
} }
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
loadAnnotations(job: any, loader: any, file: File): void {
dispatch(uploadJobAnnotationsAsync(job, loader, file));
},
showExportModal(jobInstance: any): void { 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) { removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean) {
dispatch(removeAnnotationsAsyncAction(startnumber, endnumber, delTrackKeyframesOnly)); dispatch(removeAnnotationsAsyncAction(startnumber, endnumber, delTrackKeyframesOnly));
@ -93,27 +82,18 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
const { const {
jobInstance, jobInstance,
stopFrame, stopFrame,
annotationFormats: { loaders, dumpers },
history, history,
loadActivity,
loadAnnotations,
showExportModal, showExportModal,
showImportModal,
removeAnnotations, removeAnnotations,
setForceExitAnnotationFlag, setForceExitAnnotationFlag,
saveAnnotations, saveAnnotations,
updateJob, updateJob,
} = props; } = 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 onClickMenu = (params: MenuInfo): void => {
const [action] = params.keyPath; const [action] = params.keyPath;
if (action === Actions.EXPORT_TASK_DATASET) { if (action === Actions.EXPORT_JOB_DATASET) {
showExportModal(jobInstance); showExportModal(jobInstance);
} else if (action === Actions.RENEW_JOB) { } else if (action === Actions.RENEW_JOB) {
jobInstance.state = core.enums.JobState.NEW; jobInstance.state = core.enums.JobState.NEW;
@ -131,16 +111,14 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
[, jobInstance.state] = action.split(':'); [, jobInstance.state] = action.split(':');
updateJob(jobInstance); updateJob(jobInstance);
window.location.reload(); window.location.reload();
} else if (action === Actions.LOAD_JOB_ANNO) {
showImportModal(jobInstance);
} }
}; };
return ( return (
<AnnotationMenuComponent <AnnotationMenuComponent
taskMode={jobInstance.mode} taskMode={jobInstance.mode}
loaders={loaders}
dumpers={dumpers}
loadActivity={loadActivity}
onUploadAnnotations={onUploadAnnotations}
onClickMenu={onClickMenu} onClickMenu={onClickMenu}
removeAnnotations={removeAnnotations} removeAnnotations={removeAnnotations}
setForceExitAnnotationFlag={setForceExitAnnotationFlag} setForceExitAnnotationFlag={setForceExitAnnotationFlag}

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -24,7 +25,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
countInvisible: tasks.hideEmpty ? countInvisible: tasks.hideEmpty ?
tasks.current.filter((task: Task): boolean => !task.instance.jobs.length).length : tasks.current.filter((task: Task): boolean => !task.instance.jobs.length).length :
0, 0,
importing: state.tasks.importing, importing: state.import.tasks.backup.importing,
}; };
} }

@ -8,6 +8,7 @@ import {
Label, Attribute, RawAttribute, RawLabel, Label, Attribute, RawAttribute, RawLabel,
} from 'cvat-core/src/labels'; } from 'cvat-core/src/labels';
import { ShapeType } from 'cvat-core/src/enums'; import { ShapeType } from 'cvat-core/src/enums';
import { Storage, StorageData } from 'cvat-core/src/storage';
const cvat: any = _cvat; const cvat: any = _cvat;
@ -26,9 +27,11 @@ export {
Label, Label,
Attribute, Attribute,
ShapeType, ShapeType,
Storage,
}; };
export type { export type {
RawAttribute, RawAttribute,
RawLabel, RawLabel,
StorageData,
}; };

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -937,19 +938,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}; };
} }
case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS: { case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS: {
const { states, job, history } = action.payload; const { states, history } = action.payload;
const { loads } = state.activities;
delete loads[job.id];
return { return {
...state, ...state,
activities: {
...state.activities,
loads: {
...loads,
},
},
annotations: { annotations: {
...state.annotations, ...state.annotations,
history, history,

@ -1,66 +1,180 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { ExportActions, ExportActionTypes } from 'actions/export-actions'; import { ExportActions, ExportActionTypes } from 'actions/export-actions';
import { getCore } from 'cvat-core-wrapper'; import { omit } from 'lodash';
import deepCopy from 'utils/deep-copy'; import deepCopy from 'utils/deep-copy';
import { ExportState } from '.'; import { ExportState } from '.';
import { defineActititiesField } from './import-reducer';
const core = getCore();
const defaultState: ExportState = { const defaultState: ExportState = {
tasks: {}, projects: {
projects: {}, dataset: {
instance: null, current: {},
modalVisible: false, 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 => { export default (state: ExportState = defaultState, action: ExportActions): ExportState => {
switch (action.type) { switch (action.type) {
case ExportActionTypes.OPEN_EXPORT_MODAL: case ExportActionTypes.OPEN_EXPORT_DATASET_MODAL: {
const { instance } = action.payload;
const activitiesField = defineActititiesField(instance);
return { return {
...state, ...state,
modalVisible: true, [activitiesField]: {
instance: action.payload.instance, ...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 { return {
...state, ...state,
modalVisible: false, [activitiesField]: {
instance: null, ...state[activitiesField],
dataset: {
...state[activitiesField].dataset,
modalInstance: null,
},
},
instanceType: null,
}; };
}
case ExportActionTypes.EXPORT_DATASET: { case ExportActionTypes.EXPORT_DATASET: {
const { instance, format } = action.payload; const { instance, format } = action.payload;
const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks); const field = defineActititiesField(instance) as 'projects' | 'tasks' | 'jobs';
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];
return { return {
...state, ...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_FAILED:
case ExportActionTypes.EXPORT_DATASET_SUCCESS: { case ExportActionTypes.EXPORT_DATASET_SUCCESS: {
const { instance, format } = action.payload; const { instance, format } = action.payload;
const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks); const field: 'projects' | 'tasks' | 'jobs' = defineActititiesField(instance);
const instanceId = instance instanceof core.classes.Project || const activities = deepCopy(state[field]);
instance instanceof core.classes.Task ? instance.id : instance.taskId;
activities[instanceId] = activities[instanceId].filter( activities.dataset.current[instance.id] = activities.dataset.current[instance.id].filter(
(exporterName: string): boolean => exporterName !== format, (exporterName: string): boolean => exporterName !== format,
); );
return { return {
...state, ...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: default:

@ -1,58 +1,223 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { omit } from 'lodash';
import { ImportActions, ImportActionTypes } from 'actions/import-actions'; import { ImportActions, ImportActionTypes } from 'actions/import-actions';
import { getCore } from 'cvat-core-wrapper';
import { ImportState } from '.'; 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 = { const defaultState: ImportState = {
progress: 0.0, projects: {
status: '', dataset: {
instance: null, modalInstance: null,
importingId: null, current: {},
modalVisible: false, },
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 => { export default (state: ImportState = defaultState, action: ImportActions): ImportState => {
switch (action.type) { switch (action.type) {
case ImportActionTypes.OPEN_IMPORT_MODAL: case ImportActionTypes.OPEN_IMPORT_DATASET_MODAL: {
const { instance } = action.payload;
const activitiesField = defineActititiesField(instance);
return { return {
...state, ...state,
modalVisible: true, [activitiesField]: {
instance: action.payload.instance, ...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 { return {
...state, ...state,
modalVisible: false, [activitiesField]: {
instance: null, ...state[activitiesField],
dataset: {
...state[activitiesField].dataset,
modalInstance: null,
},
},
instanceType: null,
}; };
} }
case ImportActionTypes.IMPORT_DATASET: { 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 { return {
...state, ...state,
importingId: id, [activitiesField]: {
status: 'The file is being uploaded to the server', ...state[activitiesField],
dataset: {
...state[activitiesField].dataset,
current: {
...state[activitiesField].dataset.current,
[instance.id]: updatedActivity,
},
},
},
}; };
} }
case ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS: { case ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS: {
const { progress, status } = action.payload; const { progress, status, instance } = action.payload;
const activitiesField = defineActititiesField(instance);
return { return {
...state, ...state,
progress, [activitiesField]: {
status, ...state[activitiesField],
dataset: {
...state[activitiesField].dataset,
current: {
...state[activitiesField].dataset.current,
[instance.id]: {
...state[activitiesField].dataset.current[instance.id] as Record<string, unknown>,
progress,
status,
},
},
},
},
}; };
} }
case ImportActionTypes.IMPORT_DATASET_FAILED: case ImportActionTypes.IMPORT_DATASET_FAILED:
case ImportActionTypes.IMPORT_DATASET_SUCCESS: { 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 { return {
...state, ...state,
progress: defaultState.progress, [`${instanceType}s`]: {
status: defaultState.status, ...state[field],
importingId: null, backup: {
...state[field].backup,
importing: false,
},
},
}; };
} }
default: default:

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -51,11 +52,7 @@ export interface ProjectsState {
deletes: { deletes: {
[projectId: number]: boolean; // deleted (deleting if in dictionary) [projectId: number]: boolean; // deleted (deleting if in dictionary)
}; };
backups: {
[projectId: number]: boolean;
}
}; };
restoring: boolean;
} }
export interface TasksQuery { export interface TasksQuery {
@ -88,7 +85,6 @@ export interface JobsState {
} }
export interface TasksState { export interface TasksState {
importing: boolean;
initialized: boolean; initialized: boolean;
fetching: boolean; fetching: boolean;
updating: boolean; updating: boolean;
@ -101,10 +97,6 @@ export interface TasksState {
count: number; count: number;
current: Task[]; current: Task[];
activities: { activities: {
loads: {
// only one loading simultaneously
[tid: number]: string; // loader name
};
deletes: { deletes: {
[tid: number]: boolean; // deleted (deleting if in dictionary) [tid: number]: boolean; // deleted (deleting if in dictionary)
}; };
@ -113,9 +105,6 @@ export interface TasksState {
status: string; status: string;
error: string; error: string;
}; };
backups: {
[tid: number]: boolean;
};
jobUpdates: { jobUpdates: {
[jid: number]: boolean, [jid: number]: boolean,
}; };
@ -123,22 +112,83 @@ export interface TasksState {
} }
export interface ExportState { export interface ExportState {
projects: {
dataset: {
current: {
[id: number]: string[];
};
modalInstance: any | null;
};
backup: {
current: {
[id: number]: boolean;
};
modalInstance: any | null;
};
};
tasks: { tasks: {
[tid: number]: string[]; dataset: {
current: {
[id: number]: string[];
};
modalInstance: any | null;
};
backup: {
current: {
[id: number]: boolean;
};
modalInstance: any | null;
};
}; };
projects: { jobs: {
[pid: number]: string[]; dataset: {
current: {
[id: number]: string[];
};
modalInstance: any | null;
};
}; };
instance: any; instanceType: 'project' | 'task' | 'job' | null;
modalVisible: boolean;
} }
export interface ImportState { export interface ImportState {
importingId: number | null; projects: {
progress: number; dataset: {
status: string; modalInstance: any | null;
instance: any; current: {
modalVisible: boolean; [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 { export interface FormatsState {
@ -438,10 +488,12 @@ export interface NotificationsState {
exporting: { exporting: {
dataset: null | ErrorState; dataset: null | ErrorState;
annotation: null | ErrorState; annotation: null | ErrorState;
backup: null | ErrorState;
}; };
importing: { importing: {
dataset: null | ErrorState; dataset: null | ErrorState;
annotation: null | ErrorState; annotation: null | ErrorState;
backup: null | ErrorState;
}; };
cloudStorages: { cloudStorages: {
creating: null | ErrorState; creating: null | ErrorState;
@ -478,7 +530,17 @@ export interface NotificationsState {
}; };
projects: { projects: {
restoringDone: string; 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<string, string>; normalizedKeyMap: Record<string, string>;
} }
export enum StorageLocation {
LOCAL = 'local',
CLOUD_STORAGE = 'cloud_storage',
}
export enum ReviewStatus { export enum ReviewStatus {
ACCEPTED = 'accepted', ACCEPTED = 'accepted',
REJECTED = 'rejected', REJECTED = 'rejected',

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -22,11 +23,8 @@ import { CloudStorageActionTypes } from 'actions/cloud-storage-actions';
import { OrganizationActionsTypes } from 'actions/organization-actions'; import { OrganizationActionsTypes } from 'actions/organization-actions';
import { JobsActionTypes } from 'actions/jobs-actions'; import { JobsActionTypes } from 'actions/jobs-actions';
import { getCore } from 'cvat-core-wrapper';
import { NotificationsState } from '.'; import { NotificationsState } from '.';
const core = getCore();
const defaultState: NotificationsState = { const defaultState: NotificationsState = {
errors: { errors: {
auth: { auth: {
@ -128,10 +126,12 @@ const defaultState: NotificationsState = {
exporting: { exporting: {
dataset: null, dataset: null,
annotation: null, annotation: null,
backup: null,
}, },
importing: { importing: {
dataset: null, dataset: null,
annotation: null, annotation: null,
backup: null,
}, },
cloudStorages: { cloudStorages: {
creating: null, creating: null,
@ -169,6 +169,16 @@ const defaultState: NotificationsState = {
projects: { projects: {
restoringDone: '', 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: { case ExportActionTypes.EXPORT_DATASET_FAILED: {
const instanceID = action.payload.instance.id; const { instance, instanceType } = action.payload;
const instanceType = action.payload.instance instanceof core.classes.Project ? 'project' : 'task';
return { return {
...state, ...state,
errors: { errors: {
@ -364,173 +373,203 @@ export default function (state = defaultState, action: AnyAction): Notifications
dataset: { dataset: {
message: message:
'Could not export dataset for the ' + 'Could not export dataset for the ' +
`<a href="/${instanceType}s/${instanceID}" target="_blank">` + `<a href="/${instanceType}s/${instance.id}" target="_blank">` +
`${instanceType} ${instanceID}</a>`, `${instanceType} ${instance.id}</a>`,
reason: action.payload.error.toString(), reason: action.payload.error.toString(),
}, },
}, },
}, },
}; };
} }
case ImportActionTypes.IMPORT_DATASET_FAILED: { case ExportActionTypes.EXPORT_DATASET_SUCCESS: {
const instanceID = action.payload.instance.id; const {
instance, instanceType, isLocal, resource,
} = action.payload;
const auxiliaryVerb = resource === 'Dataset' ? 'has' : 'have';
return { return {
...state, ...state,
errors: { messages: {
...state.errors, ...state.messages,
exporting: { exporting: {
...state.errors.exporting, ...state.messages.exporting,
dataset: { dataset:
message: `${resource} for ${instanceType} ${instance.id} ` +
'Could not import dataset to the ' + `${auxiliaryVerb} been ${(isLocal) ? 'downloaded' : 'uploaded'} ` +
`<a href="/projects/${instanceID}" target="_blank">` + `${(isLocal) ? 'locally' : 'to cloud storage'}`,
`project ${instanceID}</a>`,
reason: action.payload.error.toString(),
},
}, },
}, },
}; };
} }
case TasksActionTypes.GET_TASKS_FAILED: { case ExportActionTypes.EXPORT_BACKUP_FAILED: {
const { instance, instanceType } = action.payload;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { exporting: {
...state.errors.tasks, ...state.errors.exporting,
fetching: { backup: {
message: 'Could not fetch tasks', message:
`Could not export the ${instanceType}${instance.id}`,
reason: action.payload.error.toString(), reason: action.payload.error.toString(),
}, },
}, },
}, },
}; };
} }
case TasksActionTypes.LOAD_ANNOTATIONS_FAILED: { case ExportActionTypes.EXPORT_BACKUP_SUCCESS: {
const taskID = action.payload.task.id; const { instance, instanceType, isLocal } = action.payload;
return { return {
...state, ...state,
errors: { messages: {
...state.errors, ...state.messages,
tasks: { exporting: {
...state.errors.tasks, ...state.messages.exporting,
loading: { backup:
message: `Backup for the ${instanceType}${instance.id} ` +
'Could not upload annotation for the ' + `has been ${(isLocal) ? 'downloaded' : 'uploaded'} ` +
`<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`, `${(isLocal) ? 'locally' : 'to cloud storage'}`,
reason: action.payload.error.toString(),
className: 'cvat-notification-notice-load-annotation-failed',
},
}, },
}, },
}; };
} }
case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: { case ImportActionTypes.IMPORT_DATASET_SUCCESS: {
const taskID = action.payload.task.id; const { instance, resource } = action.payload;
const message = resource === 'annotation' ?
'Annotations have been loaded to the ' +
`<a href="/tasks/${instance.taskId || instance.id}" target="_blank">` +
`task ${instance.taskId || instance.id}</a>` :
'Dataset has been imported to the ' +
`<a href="/projects/${instance.id}" target="_blank">project ${instance.id}</a>`;
return { return {
...state, ...state,
messages: { messages: {
...state.messages, ...state.messages,
tasks: { importing: {
...state.messages.tasks, ...state.messages.importing,
loadingDone: [resource]: message,
'Annotations have been loaded to the ' +
`<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
}, },
}, },
}; };
} }
case TasksActionTypes.UPDATE_TASK_FAILED: { case ImportActionTypes.IMPORT_DATASET_FAILED: {
const taskID = action.payload.task.id; const { instance, resource } = action.payload;
const message = resource === 'annotation' ? 'Could not upload annotation for the ' +
`<a href="/tasks/${instance.taskId || instance.id}" target="_blank">` :
'Could not import dataset to the ' +
`<a href="/projects/${instance.id}" target="_blank">` +
`project ${instance.id}</a>`;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { importing: {
...state.errors.tasks, ...state.errors.importing,
updating: { dataset: {
message: `Could not update <a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`, message,
reason: action.payload.error.toString(), 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: { case ImportActionTypes.IMPORT_BACKUP_SUCCESS: {
const { taskID } = action.payload; const { instanceId, instanceType } = action.payload;
return {
...state,
messages: {
...state.messages,
importing: {
...state.messages.importing,
backup:
`The ${instanceType} has been restored succesfully.
Click <a href="/${instanceType}s/${instanceId}">here</a> to open`,
},
},
};
}
case ImportActionTypes.IMPORT_BACKUP_FAILED: {
const { instanceType } = action.payload;
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { importing: {
...state.errors.tasks, ...state.errors.importing,
deleting: { backup: {
message: message:
'Could not delete the ' + `Could not restore ${instanceType} backup.`,
`<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(), reason: action.payload.error.toString(),
className: 'cvat-notification-notice-delete-task-failed',
}, },
}, },
}, },
}; };
} }
case TasksActionTypes.CREATE_TASK_FAILED: { case TasksActionTypes.GET_TASKS_FAILED: {
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
creating: { fetching: {
message: 'Could not create the task', message: 'Could not fetch tasks',
reason: action.payload.error.toString(), 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 { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
exporting: { updating: {
message: 'Could not export the task', message: `Could not update <a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(), 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 { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
importing: { deleting: {
message: 'Could not import the task', message:
'Could not delete the ' +
`<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(), reason: action.payload.error.toString(),
className: 'cvat-notification-notice-delete-task-failed',
}, },
}, },
}, },
}; };
} }
case TasksActionTypes.IMPORT_TASK_SUCCESS: { case TasksActionTypes.CREATE_TASK_FAILED: {
const taskID = action.payload.task.id;
return { return {
...state, ...state,
messages: { errors: {
...state.messages, ...state.errors,
tasks: { tasks: {
...state.messages.tasks, ...state.errors.tasks,
importingDone: `Task has been imported succesfully <a href="/tasks/${taskID}">Open task</a>`, 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 <a href="/projects/${projectID}">here</a> to open`,
},
},
};
}
case FormatsActionTypes.GET_FORMATS_FAILED: { case FormatsActionTypes.GET_FORMATS_FAILED: {
return { return {
...state, ...state,

@ -1,9 +1,9 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { omit } from 'lodash';
import { ProjectsActionTypes } from 'actions/projects-actions'; import { ProjectsActionTypes } from 'actions/projects-actions';
import { BoundariesActionTypes } from 'actions/boundaries-actions'; import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { AuthActionTypes } from 'actions/auth-actions'; import { AuthActionTypes } from 'actions/auth-actions';
@ -37,9 +37,7 @@ const defaultState: ProjectsState = {
id: null, id: null,
error: '', error: '',
}, },
backups: {},
}, },
restoring: false,
}; };
export default (state: ProjectsState = defaultState, action: AnyAction): ProjectsState => { 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 BoundariesActionTypes.RESET_AFTER_ERROR:
case AuthActionTypes.LOGOUT_SUCCESS: { case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState }; return { ...defaultState };

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -31,17 +32,14 @@ const defaultState: TasksState = {
projectId: null, projectId: null,
}, },
activities: { activities: {
loads: {},
deletes: {}, deletes: {},
creates: { creates: {
taskId: null, taskId: null,
status: '', status: '',
error: '', error: '',
}, },
backups: {},
jobUpdates: {}, jobUpdates: {},
}, },
importing: false,
}; };
export default (state: TasksState = defaultState, action: AnyAction): TasksState => { export default (state: TasksState = defaultState, action: AnyAction): TasksState => {
@ -82,40 +80,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
initialized: true, initialized: true,
fetching: false, 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: { case TasksActionTypes.DELETE_TASK: {
const { taskID } = action.payload; const { taskID } = action.payload;
const { deletes } = state.activities; 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: { case TasksActionTypes.CREATE_TASK: {
return { return {
...state, ...state,

@ -1,4 +1,5 @@
# Copyright (C) 2021-2022 Intel Corporation # Copyright (C) 2021-2022 Intel Corporation
# Copyright (C) 2022 CVAT.ai Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
@ -31,7 +32,7 @@ from cvat.apps.engine.log import slogger
from cvat.apps.engine.serializers import (AttributeSerializer, DataSerializer, from cvat.apps.engine.serializers import (AttributeSerializer, DataSerializer,
LabeledDataSerializer, SegmentSerializer, SimpleJobSerializer, TaskReadSerializer, LabeledDataSerializer, SegmentSerializer, SimpleJobSerializer, TaskReadSerializer,
ProjectReadSerializer, ProjectFileSerializer, TaskFileSerializer) 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 ( from cvat.apps.engine.models import (
StorageChoice, StorageMethodChoice, DataChoice, Task, Project, Location, StorageChoice, StorageMethodChoice, DataChoice, Task, Project, Location,
CloudStorage as CloudStorageModel) 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.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.dataset_manager.bindings import CvatImportError
from cvat.apps.engine.cloud_provider import ( 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 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, return sendfile(request, file_path, attachment=True,
attachment_filename=filename) attachment_filename=filename)
elif location == Location.CLOUD_STORAGE: 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: try:
storage_id = location_conf['storage_id'] storage_id = location_conf['storage_id']
except KeyError: except KeyError:
@ -801,7 +797,7 @@ def export(db_instance, request):
db_storage = get_object_or_404(CloudStorageModel, pk=storage_id) db_storage = get_object_or_404(CloudStorageModel, pk=storage_id)
storage = db_storage_to_storage_instance(db_storage) 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) return Response(status=status.HTTP_200_OK)
else: else:
raise NotImplementedError() raise NotImplementedError()
@ -825,6 +821,14 @@ def export(db_instance, request):
result_ttl=ttl, failure_ttl=ttl) result_ttl=ttl, failure_ttl=ttl)
return Response(status=status.HTTP_202_ACCEPTED) 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): def _import(importer, request, rq_id, Serializer, file_field_name, location_conf, filename=None):
queue = django_rq.get_queue("default") queue = django_rq.get_queue("default")
rq_job = queue.fetch_job(rq_id) 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: if not rq_job:
org_id = getattr(request.iam_context['organization'], 'id', None) org_id = getattr(request.iam_context['organization'], 'id', None)
fd = None fd = None
dependent_job = None
location = location_conf.get('location') location = location_conf.get('location')
if location == Location.LOCAL: 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(): for chunk in payload_file.chunks():
f.write(chunk) f.write(chunk)
else: 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') file_name = request.query_params.get('filename')
assert file_name assert file_name, "The filename wasn't specified"
# download file from cloud storage
try: try:
storage_id = location_conf['storage_id'] storage_id = location_conf['storage_id']
except KeyError: 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' 'Cloud storage location was selected for destination'
' but cloud storage id was not specified') ' but cloud storage id was not specified')
db_storage = get_object_or_404(CloudStorageModel, pk=storage_id) db_storage = get_object_or_404(CloudStorageModel, pk=storage_id)
storage = db_storage_to_storage_instance(db_storage) key = filename
data = _import_from_cloud_storage(storage, file_name)
fd, filename = mkstemp(prefix='cvat_', dir=settings.TMP_FILES_ROOT) fd, filename = mkstemp(prefix='cvat_', dir=settings.TMP_FILES_ROOT)
with open(filename, 'wb+') as f: dependent_job = configure_dependent_job(
f.write(data.getbuffer()) queue, rq_id, _download_file_from_bucket,
db_storage, filename, key)
rq_job = queue.enqueue_call( rq_job = queue.enqueue_call(
func=importer, func=importer,
@ -875,6 +872,7 @@ def _import(importer, request, rq_id, Serializer, file_field_name, location_conf
'tmp_file': filename, 'tmp_file': filename,
'tmp_file_descriptor': fd, 'tmp_file_descriptor': fd,
}, },
depends_on=dependent_job
) )
else: else:
if rq_job.is_finished: 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']) os.remove(rq_job.meta['tmp_file'])
rq_job.delete() rq_job.delete()
return Response({'id': project_id}, status=status.HTTP_201_CREATED) return Response({'id': project_id}, status=status.HTTP_201_CREATED)
elif rq_job.is_failed: elif rq_job.is_failed or \
if rq_job.meta['tmp_file_descriptor']: os.close(rq_job.meta['tmp_file_descriptor']) rq_job.is_deferred and rq_job.dependency and rq_job.dependency.is_failed:
os.remove(rq_job.meta['tmp_file']) exc_info = process_failed_job(rq_job)
exc_info = str(rq_job.exc_info)
rq_job.delete()
# RQ adds a prefix with exception class name # RQ adds a prefix with exception class name
import_error_prefix = '{}.{}'.format( import_error_prefix = '{}.{}'.format(
CvatImportError.__module__, CvatImportError.__name__) CvatImportError.__module__, CvatImportError.__name__)

@ -648,3 +648,11 @@ def db_storage_to_storage_instance(db_storage):
'specific_attributes': db_storage.get_specific_attributes() 'specific_attributes': db_storage.get_specific_attributes()
} }
return get_cloud_storage_instance(cloud_provider=db_storage.provider_type, **details) 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)

@ -279,6 +279,11 @@ class AnnotationMixin:
return Response(serializer.data) return Response(serializer.data)
def import_annotations(self, request, pk, db_obj, import_func, rq_func, rq_id): 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_default_location = request.query_params.get('use_default_location', True)
use_settings = strtobool(str(use_default_location)) use_settings = strtobool(str(use_default_location))
obj = db_obj if use_settings else request.query_params obj = db_obj if use_settings else request.query_params

@ -108,3 +108,28 @@ def parse_specific_attributes(specific_attributes):
return { return {
key: value for (key, value) in parsed_specific_attributes key: value for (key, value) in parsed_specific_attributes
} if parsed_specific_attributes else dict() } 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

@ -44,7 +44,9 @@ from django_sendfile import sendfile
import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager as dm
import cvat.apps.dataset_manager.views # pylint: disable=unused-import import cvat.apps.dataset_manager.views # pylint: disable=unused-import
from cvat.apps.engine.cloud_provider 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.bindings import CvatImportError
from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer
from cvat.apps.engine.frame_provider import FrameProvider from cvat.apps.engine.frame_provider import FrameProvider
@ -67,9 +69,10 @@ from cvat.apps.engine.serializers import (
ProjectFileSerializer, TaskFileSerializer) ProjectFileSerializer, TaskFileSerializer)
from utils.dataset_manifest import ImageManifestManager 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 import backup
from cvat.apps.engine.mixins import PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin 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 . import models, task
from .log import clogger, slogger from .log import clogger, slogger
@ -392,14 +395,16 @@ class ProjectViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
elif rq_job.is_finished: elif rq_job.is_finished:
if rq_job.meta['tmp_file_descriptor']: os.close(rq_job.meta['tmp_file_descriptor']) if rq_job.meta['tmp_file_descriptor']: os.close(rq_job.meta['tmp_file_descriptor'])
os.remove(rq_job.meta['tmp_file']) os.remove(rq_job.meta['tmp_file'])
if rq_job.dependency:
rq_job.dependency.delete()
rq_job.delete() rq_job.delete()
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED)
elif rq_job.is_failed: elif rq_job.is_failed or \
if rq_job.meta['tmp_file_descriptor']: os.close(rq_job.meta['tmp_file_descriptor']) rq_job.is_deferred and rq_job.dependency and rq_job.dependency.is_failed:
os.remove(rq_job.meta['tmp_file']) exc_info = process_failed_job(rq_job)
rq_job.delete()
return Response( return Response(
data=str(rq_job.exc_info), data=str(exc_info),
status=status.HTTP_500_INTERNAL_SERVER_ERROR status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
else: else:
@ -837,7 +842,6 @@ class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
# UploadMixin method # UploadMixin method
def upload_finished(self, request): def upload_finished(self, request):
if self.action == 'annotations': if self.action == 'annotations':
# db_task = self.get_object()
format_name = request.query_params.get("format", "") format_name = request.query_params.get("format", "")
filename = request.query_params.get("filename", "") filename = request.query_params.get("filename", "")
tmp_dir = self._object.get_tmp_dirname() tmp_dir = self._object.get_tmp_dirname()
@ -1073,12 +1077,18 @@ class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
elif request.method == 'PUT': elif request.method == 'PUT':
format_name = request.query_params.get('format') format_name = request.query_params.get('format')
if format_name: 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( return _import_annotations(
request=request, request=request,
rq_id="{}@/api/tasks/{}/annotations/upload".format(request.user, pk), rq_id="{}@/api/tasks/{}/annotations/upload".format(request.user, pk),
rq_func=dm.task.import_task_annotations, rq_func=dm.task.import_task_annotations,
pk=pk, pk=pk,
format_name=format_name, format_name=format_name,
location_conf=location_conf
) )
else: else:
serializer = LabeledDataSerializer(data=request.data) serializer = LabeledDataSerializer(data=request.data)
@ -1417,12 +1427,18 @@ class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
elif request.method == 'PUT': elif request.method == 'PUT':
format_name = request.query_params.get('format', '') format_name = request.query_params.get('format', '')
if format_name: 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( return _import_annotations(
request=request, request=request,
rq_id="{}@/api/jobs/{}/annotations/upload".format(request.user, pk), rq_id="{}@/api/jobs/{}/annotations/upload".format(request.user, pk),
rq_func=dm.task.import_job_annotations, rq_func=dm.task.import_job_annotations,
pk=pk, pk=pk,
format_name=format_name format_name=format_name,
location_conf=location_conf
) )
else: else:
serializer = LabeledDataSerializer(data=request.data) serializer = LabeledDataSerializer(data=request.data)
@ -2115,13 +2131,12 @@ def rq_handler(job, exc_type, exc_value, tb):
return True return True
@validate_bucket_status def _download_file_from_bucket(db_storage, filename, key):
def _export_to_cloud_storage(storage, file_path, file_name): storage = db_storage_to_storage_instance(db_storage)
storage.upload_file(file_path, file_name)
@validate_bucket_status data = import_from_cloud_storage(storage, key)
def _import_from_cloud_storage(storage, file_name): with open(filename, 'wb+') as f:
return storage.download_fileobj(file_name) f.write(data.getbuffer())
def _import_annotations(request, rq_id, rq_func, pk, format_name, def _import_annotations(request, rq_id, rq_func, pk, format_name,
filename=None, location_conf=None): 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 # Then we dont need to create temporary file
# Or filename specify key in cloud storage so we need to download file # Or filename specify key in cloud storage so we need to download file
fd = None fd = None
dependent_job = None
location = location_conf.get('location') if location_conf else Location.LOCAL location = location_conf.get('location') if location_conf else Location.LOCAL
if not filename or location == Location.CLOUD_STORAGE: 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(): for chunk in anno_file.chunks():
f.write(chunk) f.write(chunk)
else: else:
# download annotation file from cloud storage assert filename, 'The filename was not spesified'
try: try:
storage_id = location_conf['storage_id'] storage_id = location_conf['storage_id']
except KeyError: except KeyError:
raise serializer.ValidationError( raise serializers.ValidationError(
'Cloud storage location was selected for destination' 'Cloud storage location was selected for destination'
' but cloud storage id was not specified') ' but cloud storage id was not specified')
db_storage = get_object_or_404(CloudStorageModel, pk=storage_id) db_storage = get_object_or_404(CloudStorageModel, pk=storage_id)
storage = db_storage_to_storage_instance(db_storage) key = filename
assert filename, 'filename was not spesified'
data = _import_from_cloud_storage(storage, filename)
fd, filename = mkstemp(prefix='cvat_{}'.format(pk), dir=settings.TMP_FILES_ROOT) fd, filename = mkstemp(prefix='cvat_{}'.format(pk), dir=settings.TMP_FILES_ROOT)
with open(filename, 'wb+') as f: dependent_job = configure_dependent_job(
f.write(data.getbuffer()) queue, rq_id, _download_file_from_bucket,
db_storage, filename, key)
av_scan_paths(filename) av_scan_paths(filename)
rq_job = queue.enqueue_call( rq_job = queue.enqueue_call(
func=rq_func, func=rq_func,
args=(pk, filename, format_name), 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'] = filename
rq_job.meta['tmp_file_descriptor'] = fd 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']) os.remove(rq_job.meta['tmp_file'])
rq_job.delete() rq_job.delete()
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED)
elif rq_job.is_failed: elif rq_job.is_failed or \
if rq_job.meta['tmp_file_descriptor']: os.close(rq_job.meta['tmp_file_descriptor']) rq_job.is_deferred and rq_job.dependency and rq_job.dependency.is_failed:
os.remove(rq_job.meta['tmp_file']) exc_info = process_failed_job(rq_job)
exc_info = str(rq_job.exc_info)
rq_job.delete()
# RQ adds a prefix with exception class name # RQ adds a prefix with exception class name
import_error_prefix = '{}.{}'.format( import_error_prefix = '{}.{}'.format(
CvatImportError.__module__, CvatImportError.__name__) 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.__class__.__name__.lower(),
db_instance.name if isinstance(db_instance, (Task, Project)) else db_instance.id, db_instance.name if isinstance(db_instance, (Task, Project)) else db_instance.id,
timestamp, format_name, osp.splitext(file_path)[1] timestamp, format_name, osp.splitext(file_path)[1]
) ).lower()
# save annotation to specified location # save annotation to specified location
location = location_conf.get('location') location = location_conf.get('location')
if location == Location.LOCAL: if location == Location.LOCAL:
return sendfile(request, file_path, attachment=True, return sendfile(request, file_path, attachment=True,
attachment_filename=filename.lower()) attachment_filename=filename)
elif location == Location.CLOUD_STORAGE: elif location == Location.CLOUD_STORAGE:
try: try:
storage_id = location_conf['storage_id'] 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) db_storage = get_object_or_404(CloudStorageModel, pk=storage_id)
storage = db_storage_to_storage_instance(db_storage) 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) return Response(status=status.HTTP_200_OK)
else: else:
raise NotImplementedError() raise NotImplementedError()
@ -2309,6 +2320,7 @@ def _import_project_dataset(request, rq_id, rq_func, pk, format_name, filename=N
if not rq_job: if not rq_job:
fd = None fd = None
dependent_job = None
location = location_conf.get('location') if location_conf else None location = location_conf.get('location') if location_conf else None
if not filename and location != Location.CLOUD_STORAGE: if not filename and location != Location.CLOUD_STORAGE:
serializer = DatasetFileSerializer(data=request.data) 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(): for chunk in dataset_file.chunks():
f.write(chunk) f.write(chunk)
elif location == Location.CLOUD_STORAGE: elif location == Location.CLOUD_STORAGE:
assert filename assert filename, 'The filename was not spesified'
# download project file from cloud storage
try: try:
storage_id = location_conf['storage_id'] storage_id = location_conf['storage_id']
except KeyError: 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' 'Cloud storage location was selected for destination'
' but cloud storage id was not specified') ' but cloud storage id was not specified')
db_storage = get_object_or_404(CloudStorageModel, pk=storage_id) db_storage = get_object_or_404(CloudStorageModel, pk=storage_id)
storage = db_storage_to_storage_instance(db_storage) key = filename
fd, filename = mkstemp(prefix='cvat_{}'.format(pk), dir=settings.TMP_FILES_ROOT)
data = _import_from_cloud_storage(storage, filename) dependent_job = configure_dependent_job(
queue, rq_id, _download_file_from_bucket,
fd, filename = mkstemp(prefix='cvat_', dir=settings.TMP_FILES_ROOT) db_storage, filename, key)
with open(filename, 'wb+') as f:
f.write(data.getbuffer())
rq_job = queue.enqueue_call( rq_job = queue.enqueue_call(
func=rq_func, func=rq_func,
args=(pk, filename, format_name), args=(pk, filename, format_name),
job_id=rq_id, job_id=rq_id,
meta={ meta={
'tmp_file': filename, 'tmp_file': filename,
'tmp_file_descriptor': fd, 'tmp_file_descriptor': fd,
}, },
) depends_on=dependent_job
)
else: else:
return Response(status=status.HTTP_409_CONFLICT, data='Import job already exists') return Response(status=status.HTTP_409_CONFLICT, data='Import job already exists')

@ -98,6 +98,7 @@ context('Export project dataset.', { browser: '!firefox' }, () => {
datasetArchiveName = file; datasetArchiveName = file;
cy.verifyDownload(datasetArchiveName); cy.verifyDownload(datasetArchiveName);
}); });
cy.verifyNotification();
}); });
it('Export project dataset. Dataset.', () => { it('Export project dataset. Dataset.', () => {
@ -143,6 +144,7 @@ context('Export project dataset.', { browser: '!firefox' }, () => {
archive: datasetArchiveName, archive: datasetArchiveName,
}; };
cy.importProject(importDataset); cy.importProject(importDataset);
cy.verifyNotification();
cy.openProject(projectName); cy.openProject(projectName);
cy.get('.cvat-tasks-list-item').should('have.length', 1); cy.get('.cvat-tasks-list-item').should('have.length', 1);
}); });
@ -172,6 +174,7 @@ context('Export project dataset.', { browser: '!firefox' }, () => {
archive: datasetArchiveName, archive: datasetArchiveName,
}; };
cy.importProject(importDataset); cy.importProject(importDataset);
cy.verifyNotification();
cy.openProject(projectName); cy.openProject(projectName);
cy.get('.cvat-tasks-list-item').should('have.length', 1); cy.get('.cvat-tasks-list-item').should('have.length', 1);
cy.get('.cvat-constructor-viewer-item') cy.get('.cvat-constructor-viewer-item')

@ -76,6 +76,7 @@ context('Export project dataset with 3D task.', { browser: '!firefox' }, () => {
datasetArchiveName = file; datasetArchiveName = file;
cy.verifyDownload(datasetArchiveName); cy.verifyDownload(datasetArchiveName);
}); });
cy.verifyNotification();
}); });
it('Export project with 3D task. Annotation. Rename a archive.', () => { 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, archive: datasetArchiveName,
}; };
cy.importProject(importDataset); cy.importProject(importDataset);
cy.verifyNotification();
cy.openProject(projectName); cy.openProject(projectName);
cy.get('.cvat-tasks-list-item').should('have.length', 1); cy.get('.cvat-tasks-list-item').should('have.length', 1);
}); });

@ -92,6 +92,7 @@ context('Backup, restore a project.', { browser: '!firefox' }, () => {
projectBackupArchiveFullName = file; projectBackupArchiveFullName = file;
cy.verifyDownload(projectBackupArchiveFullName); cy.verifyDownload(projectBackupArchiveFullName);
}); });
cy.verifyNotification();
}); });
it('Remove and restore the project from backup.', () => { 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; projectBackupArchiveFullName = file;
cy.verifyDownload(projectBackupArchiveFullName); cy.verifyDownload(projectBackupArchiveFullName);
}); });
cy.verifyNotification();
}); });
it('Remove and restore the project from backup.', () => { it('Remove and restore the project from backup.', () => {

@ -43,18 +43,13 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => {
.parents('.cvat-tasks-list-item') .parents('.cvat-tasks-list-item')
.find('.cvat-menu-icon') .find('.cvat-menu-icon')
.trigger('mouseover'); .trigger('mouseover');
cy.contains('Upload annotations').trigger('mouseover'); cy.contains('Upload annotations').click();
cy.contains('.cvat-menu-load-submenu-item', exportFormat.split(' ')[0]) cy.get('.cvat-modal-import-dataset').find('.cvat-modal-import-select').click();
.should('be.visible') cy.contains('.cvat-modal-import-dataset-option-item', exportFormat.split(' ')[0]).click();
.within(() => { cy.get('.cvat-modal-import-select').should('contain.text', exportFormat.split(' ')[0]);
cy.get('.cvat-menu-load-submenu-item-button').click(); cy.get('input[type="file"]').attachFile(annotationArchiveNameCustomeName, { subjectType: 'drag-n-drop' });
}); cy.get(`[title="${annotationArchiveNameCustomeName}"]`).should('be.visible');
// when a user clicks, menu is closing and it triggers rerender cy.contains('button', 'OK').click();
// 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);
});
} }
function confirmUpdate(modalWindowClassName) { function confirmUpdate(modalWindowClassName) {
@ -79,11 +74,12 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => {
format: exportFormat, format: exportFormat,
archiveCustomeName: 'task_export_annotation_custome_name', archiveCustomeName: 'task_export_annotation_custome_name',
}; };
cy.exportTask(exportAnnotationRenameArchive); cy.exportJob(exportAnnotationRenameArchive);
cy.getDownloadFileName().then((file) => { cy.getDownloadFileName().then((file) => {
annotationArchiveNameCustomeName = file; annotationArchiveNameCustomeName = file;
cy.verifyDownload(annotationArchiveNameCustomeName); cy.verifyDownload(annotationArchiveNameCustomeName);
}); });
cy.verifyNotification();
}); });
it('Save job. Dump annotation. Remove annotation. Save job.', () => { it('Save job. Dump annotation. Remove annotation. Save job.', () => {
@ -92,11 +88,12 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => {
type: 'annotations', type: 'annotations',
format: exportFormat, format: exportFormat,
}; };
cy.exportTask(exportAnnotation); cy.exportJob(exportAnnotation);
cy.getDownloadFileName().then((file) => { cy.getDownloadFileName().then((file) => {
annotationArchiveName = file; annotationArchiveName = file;
cy.verifyDownload(annotationArchiveName); cy.verifyDownload(annotationArchiveName);
}); });
cy.verifyNotification();
cy.removeAnnotations(); cy.removeAnnotations();
cy.saveJob('PUT'); cy.saveJob('PUT');
cy.get('#cvat_canvas_shape_1').should('not.exist'); cy.get('#cvat_canvas_shape_1').should('not.exist');
@ -105,20 +102,19 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => {
it('Upload annotation to job.', () => { it('Upload annotation to job.', () => {
cy.interactMenu('Upload annotations'); cy.interactMenu('Upload annotations');
cy.contains('.cvat-menu-load-submenu-item', exportFormat.split(' ')[0]) cy.get('.cvat-modal-import-dataset');
.should('be.visible') cy.get('.cvat-modal-import-select').click();
.within(() => { cy.contains('.cvat-modal-import-dataset-option-item', exportFormat.split(' ')[0]).click();
cy.get('.cvat-menu-load-submenu-item-button').click(); cy.get('.cvat-modal-import-select').should('contain.text', exportFormat.split(' ')[0]);
}); cy.get('input[type="file"]').attachFile(annotationArchiveName, { subjectType: 'drag-n-drop' });
// when a user clicks, menu is closing and it triggers rerender cy.get(`[title="${annotationArchiveName}"]`).should('be.visible');
// we use mouseout here to emulate user behaviour cy.contains('button', 'OK').click();
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);
});
confirmUpdate('.cvat-modal-content-load-job-annotation'); confirmUpdate('.cvat-modal-content-load-job-annotation');
cy.intercept('GET', '/api/jobs/**/annotations**').as('uploadAnnotationsGet'); 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.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200);
cy.verifyNotification();
cy.get('#cvat_canvas_shape_1').should('exist'); cy.get('#cvat_canvas_shape_1').should('exist');
cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations(); cy.removeAnnotations();
@ -130,8 +126,9 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => {
cy.goToTaskList(); cy.goToTaskList();
uploadToTask(taskName); uploadToTask(taskName);
confirmUpdate('.cvat-modal-content-load-task-annotation'); confirmUpdate('.cvat-modal-content-load-task-annotation');
cy.contains('Annotations have been loaded').should('be.visible'); cy.get('.cvat-notification-notice-import-annotation-start').should('be.visible');
cy.get('[data-icon="close"]').click(); cy.closeNotification('.cvat-notification-notice-import-annotation-start');
cy.verifyNotification();
cy.openTaskJob(taskName, 0, false); cy.openTaskJob(taskName, 0, false);
cy.get('#cvat_canvas_shape_1').should('exist'); cy.get('#cvat_canvas_shape_1').should('exist');
cy.get('#cvat-objects-sidebar-state-item-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); cy.createAnnotationTask(taskNameSecond, labelNameSecond, attrName, textDefaultValue, archiveName);
uploadToTask(taskNameSecond); uploadToTask(taskNameSecond);
confirmUpdate('.cvat-modal-content-load-task-annotation'); 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') cy.get('.cvat-notification-notice-load-annotation-failed')
.should('exist') .should('exist')
.find('[aria-label="close"]') .find('[aria-label="close"]')

@ -36,11 +36,29 @@ context('Import annotations for frames with dots in name.', { browser: '!firefox
let annotationArchiveName = ''; let annotationArchiveName = '';
function confirmUpdate(modalWindowClassName) { function confirmUpdate(modalWindowClassName) {
cy.get(modalWindowClassName).within(() => { cy.get(modalWindowClassName).should('be.visible').within(() => {
cy.contains('button', 'Update').click(); 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(() => { before(() => {
cy.visit('auth/login'); cy.visit('auth/login');
cy.login(); cy.login();
@ -65,22 +83,21 @@ context('Import annotations for frames with dots in name.', { browser: '!firefox
describe(`Testing case "${issueId}"`, () => { describe(`Testing case "${issueId}"`, () => {
it('Save job. Dump annotation to YOLO format. Remove annotation. Save job.', () => { it('Save job. Dump annotation to YOLO format. Remove annotation. Save job.', () => {
cy.saveJob('PATCH', 200, 'saveJobDump'); cy.saveJob('PATCH', 200, 'saveJobDump');
cy.intercept('GET', '/api/tasks/**/annotations**').as('dumpAnnotations'); cy.intercept('GET', '/api/jobs/**/annotations**').as('dumpAnnotations');
cy.interactMenu('Export task dataset'); cy.interactMenu('Export job dataset');
cy.get('.cvat-modal-export-task').find('.cvat-modal-export-select').click(); cy.get('.cvat-modal-export-select').click();
cy.get('.ant-select-dropdown') cy.get('.ant-select-dropdown')
.not('.ant-select-dropdown-hidden') .not('.ant-select-dropdown-hidden');
.within(() => { cy.get('.rc-virtual-list-holder')
cy.get('.rc-virtual-list-holder') .contains('.cvat-modal-export-option-item', dumpType)
.contains('.cvat-modal-export-option-item', dumpType) .click();
.scrollIntoView()
.should('be.visible')
.click();
});
cy.get('.cvat-modal-export-select').should('contain.text', dumpType); 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', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@dumpAnnotations').its('response.statusCode').should('equal', 201); cy.wait('@dumpAnnotations').its('response.statusCode').should('equal', 201);
cy.verifyNotification();
cy.removeAnnotations(); cy.removeAnnotations();
cy.saveJob('PUT'); cy.saveJob('PUT');
cy.get('#cvat_canvas_shape_1').should('not.exist'); 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.', () => { it('Upload annotation with YOLO format to job.', () => {
cy.interactMenu('Upload annotations'); cy.interactMenu('Upload annotations');
cy.contains('.cvat-menu-load-submenu-item', dumpType.split(' ')[0]) uploadAnnotation(
.scrollIntoView() dumpType.split(' ')[0],
.should('be.visible') annotationArchiveName,
.within(() => { '.cvat-modal-content-load-job-annotation',
cy.get('.cvat-menu-load-submenu-item-button') );
.click()
.get('input[type=file]')
.attachFile(annotationArchiveName);
});
cy.intercept('GET', '/api/jobs/**/annotations?**').as('uploadAnnotationsGet'); 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.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-notification-notice-upload-annotations-fail').should('not.exist');
cy.get('#cvat_canvas_shape_1').should('exist'); cy.get('#cvat_canvas_shape_1').should('exist');
cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist');

@ -85,26 +85,38 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => {
cy.get('.ant-dropdown') cy.get('.ant-dropdown')
.not('.ant-dropdown-hidden') .not('.ant-dropdown-hidden')
.within(() => { .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) => { cy.getDownloadFileName().then((file) => {
taskBackupArchiveFullName = file; taskBackupArchiveFullName = file;
cy.verifyDownload(taskBackupArchiveFullName); cy.verifyDownload(taskBackupArchiveFullName);
}); });
cy.verifyNotification();
cy.deleteTask(taskName); cy.deleteTask(taskName);
}); });
it('Import the task. Check id, labels, shape.', () => { it('Import the task. Check id, labels, shape.', () => {
cy.intercept({ method: /PATCH|POST/, url: /\/api\/tasks\/backup.*/ }).as('importTask'); cy.intercept({ method: /PATCH|POST/, url: /\/api\/tasks\/backup.*/ }).as('importTask');
cy.get('.cvat-create-task-dropdown').click(); 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', 202);
cy.wait('@importTask').its('response.statusCode').should('equal', 201); 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', 204);
cy.wait('@importTask').its('response.statusCode').should('equal', 202); cy.wait('@importTask').its('response.statusCode').should('equal', 202);
cy.wait('@importTask', { timeout: 5000 }).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.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.openTask(taskName);
cy.url().then((link) => { cy.url().then((link) => {
expect(Number(link.split('/').slice(-1)[0])).to.be.equal(taskId + 1); expect(Number(link.split('/').slice(-1)[0])).to.be.equal(taskId + 1);

@ -26,24 +26,24 @@ context('Export task dataset.', () => {
}); });
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Export a task as dataset.', () => { it('Export a job as dataset.', () => {
const exportDataset = { const exportDataset = {
as: 'exportDataset', as: 'exportDataset',
type: 'dataset', type: 'dataset',
format: exportFormat, format: exportFormat,
}; };
cy.exportTask(exportDataset); cy.exportJob(exportDataset);
cy.waitForDownload(); cy.waitForDownload();
}); });
it('Export a task as dataset with renaming the archive.', () => { it('Export a job as dataset with renaming the archive.', () => {
const exportDataset = { const exportDataset = {
as: 'exportDatasetRenameArchive', as: 'exportDatasetRenameArchive',
type: 'dataset', type: 'dataset',
format: exportFormat, format: exportFormat,
archiveCustomeName: 'task_export_dataset_custome_name', archiveCustomeName: 'job_export_dataset_custome_name',
}; };
cy.exportTask(exportDataset); cy.exportJob(exportDataset);
cy.waitForDownload(); cy.waitForDownload();
}); });
}); });

@ -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(() => { before(() => {
cy.openTask(taskName); cy.openTask(taskName);
cy.openJob(); cy.openJob();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000); // Waiting for the point cloud to display cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams); cy.create3DCuboid(cuboidCreationParams);
cy.saveJob('PATCH', 200, 'saveJob'); cy.saveJob('PATCH', 200, 'saveJob');
@ -36,11 +50,12 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format',
type: 'annotations', type: 'annotations',
format: dumpTypePC, format: dumpTypePC,
}; };
cy.exportTask(exportAnnotation); cy.exportJob(exportAnnotation);
cy.getDownloadFileName().then((file) => { cy.getDownloadFileName().then((file) => {
annotationPCArchiveName = file; annotationPCArchiveName = file;
cy.verifyDownload(annotationPCArchiveName); cy.verifyDownload(annotationPCArchiveName);
}); });
cy.verifyNotification();
}); });
it('Export with "Point Cloud" format. Renaming the archive', () => { 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', as: 'exportAnnotationsRenameArchive',
type: 'annotations', type: 'annotations',
format: dumpTypePC, 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) => { cy.getDownloadFileName().then((file) => {
annotationPCArchiveCustomeName = file; annotationPCArchiveCustomeName = file;
cy.verifyDownload(annotationPCArchiveCustomeName); cy.verifyDownload(annotationPCArchiveCustomeName);
}); });
cy.verifyNotification();
cy.removeAnnotations(); cy.removeAnnotations();
cy.saveJob('PUT'); cy.saveJob('PUT');
cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist');
}); });
it('Upload "Point Cloud" format annotation to job.', () => { 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.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.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200);
cy.verifyNotification();
cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations(); cy.removeAnnotations();
cy.get('button').contains('Save').click().trigger('mouseout'); 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') .parents('.cvat-tasks-list-item')
.find('.cvat-menu-icon') .find('.cvat-menu-icon')
.trigger('mouseover'); .trigger('mouseover');
cy.contains('Upload annotations').trigger('mouseover'); cy.contains('Upload annotations').click();
cy.readFile(`cypress/fixtures/${annotationPCArchiveCustomeName}`, 'binary') uploadAnnotation(
.then(Cypress.Blob.binaryStringToBlob) dumpTypePC.split(' ')[0],
.then((fileContent) => { annotationPCArchiveName,
cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0]) '.cvat-modal-content-load-task-annotation',
.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('Annotations have been loaded').should('be.visible'); 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.openTaskJob(taskName);
cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations(); cy.removeAnnotations();

@ -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(() => { before(() => {
cy.openTask(taskName); cy.openTask(taskName);
cy.openJob(); cy.openJob();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000); // Waiting for the point cloud to display cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams); cy.create3DCuboid(cuboidCreationParams);
cy.saveJob('PATCH', 200, 'saveJob'); cy.saveJob('PATCH', 200, 'saveJob');
@ -36,11 +50,12 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form
type: 'annotations', type: 'annotations',
format: dumpTypeVC, format: dumpTypeVC,
}; };
cy.exportTask(exportAnnotation); cy.exportJob(exportAnnotation);
cy.getDownloadFileName().then((file) => { cy.getDownloadFileName().then((file) => {
annotationVCArchiveName = file; annotationVCArchiveName = file;
cy.verifyDownload(annotationVCArchiveName); cy.verifyDownload(annotationVCArchiveName);
}); });
cy.verifyNotification();
}); });
it('Export with "Point Cloud" format. Renaming the archive', () => { 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', as: 'exportAnnotationsRenameArchive',
type: 'annotations', type: 'annotations',
format: dumpTypeVC, 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) => { cy.getDownloadFileName().then((file) => {
annotationVCArchiveNameCustomeName = file; annotationVCArchiveNameCustomeName = file;
cy.verifyDownload(annotationVCArchiveNameCustomeName); cy.verifyDownload(annotationVCArchiveNameCustomeName);
}); });
cy.verifyNotification();
cy.removeAnnotations(); cy.removeAnnotations();
cy.saveJob('PUT'); cy.saveJob('PUT');
cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist');
}); });
it('Upload "Velodyne Points" format annotation to job.', () => { 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.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.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.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations(); cy.removeAnnotations();
cy.get('button').contains('Save').click().trigger('mouseout'); 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') .parents('.cvat-tasks-list-item')
.find('.cvat-menu-icon') .find('.cvat-menu-icon')
.trigger('mouseover'); .trigger('mouseover');
cy.contains('Upload annotations').trigger('mouseover'); cy.contains('Upload annotations').click();
cy.readFile(`cypress/fixtures/${annotationVCArchiveNameCustomeName}`, 'binary') uploadAnnotation(
.then(Cypress.Blob.binaryStringToBlob) dumpTypeVC.split(' ')[0],
.then((fileContent) => { annotationVCArchiveNameCustomeName,
cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0]) '.cvat-modal-content-load-task-annotation',
.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('Annotations have been loaded').should('be.visible'); 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.openTaskJob(taskName);
cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations(); cy.removeAnnotations();

@ -18,6 +18,7 @@ context('Canvas 3D functionality. Export as a dataset.', () => {
before(() => { before(() => {
cy.openTask(taskName); cy.openTask(taskName);
cy.openJob(); cy.openJob();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000); // Waiting for the point cloud to display cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams); cy.create3DCuboid(cuboidCreationParams);
cy.saveJob(); cy.saveJob();
@ -30,7 +31,7 @@ context('Canvas 3D functionality. Export as a dataset.', () => {
type: 'dataset', type: 'dataset',
format: dumpTypePC, format: dumpTypePC,
}; };
cy.exportTask(exportDatasetPCFormat); cy.exportJob(exportDatasetPCFormat);
cy.waitForDownload(); cy.waitForDownload();
}); });
@ -40,7 +41,7 @@ context('Canvas 3D functionality. Export as a dataset.', () => {
type: 'dataset', type: 'dataset',
format: dumpTypeVC, format: dumpTypeVC,
}; };
cy.exportTask(exportDatasetVCFormat); cy.exportJob(exportDatasetVCFormat);
cy.waitForDownload(); cy.waitForDownload();
}); });
@ -49,9 +50,9 @@ context('Canvas 3D functionality. Export as a dataset.', () => {
as: 'exportDatasetVCFormatRenameArchive', as: 'exportDatasetVCFormatRenameArchive',
type: 'dataset', type: 'dataset',
format: dumpTypeVC, 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.waitForDownload();
cy.removeAnnotations(); cy.removeAnnotations();
cy.saveJob('PUT'); cy.saveJob('PUT');

@ -35,12 +35,12 @@ context('Dump annotation if cuboid created.', () => {
type: 'annotations', type: 'annotations',
format: exportFormat, format: exportFormat,
}; };
cy.exportTask(exportAnnotation); cy.exportJob(exportAnnotation);
cy.waitForDownload(); cy.waitForDownload();
}); });
it('Error notification is not exists.', () => { it('Error notification is not exists.', () => {
cy.get('.ant-notification-notice').should('not.exist'); cy.get('.ant-notification-notice-error').should('not.exist');
}); });
}); });
}); });

@ -864,7 +864,7 @@ Cypress.Commands.add('exportTask', ({
cy.contains('.cvat-modal-export-option-item', format).should('be.visible').click(); 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); cy.get('.cvat-modal-export-task').find('.cvat-modal-export-select').should('contain.text', format);
if (type === 'dataset') { 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) { if (archiveCustomeName) {
cy.get('.cvat-modal-export-task').find('.cvat-modal-export-filename-input').type(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'); 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) => { Cypress.Commands.add('renameTask', (oldName, newName) => {
cy.get('.cvat-task-details-task-name').within(() => { cy.get('.cvat-task-details-task-name').within(() => {
cy.get('[aria-label="edit"]').click(); 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); 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) => { Cypress.Commands.overwrite('visit', (orig, url, options) => {
orig(url, options); orig(url, options);
cy.closeModalUnsupportedPlatform(); cy.closeModalUnsupportedPlatform();

@ -104,13 +104,14 @@ Cypress.Commands.add('exportProject', ({
cy.contains('.cvat-modal-export-option-item', dumpType).should('be.visible').click(); cy.contains('.cvat-modal-export-option-item', dumpType).should('be.visible').click();
cy.get('.cvat-modal-export-select').should('contain.text', dumpType); cy.get('.cvat-modal-export-select').should('contain.text', dumpType);
if (type === 'dataset') { 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) { if (archiveCustomeName) {
cy.get('.cvat-modal-export-project').find('.cvat-modal-export-filename-input').type(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-modal-export-project').contains('button', 'OK').click();
cy.get('.cvat-notification-notice-export-project-start').should('be.visible'); cy.get('.cvat-notification-notice-export-project-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-export-project-start');
}); });
Cypress.Commands.add('importProject', ({ Cypress.Commands.add('importProject', ({
@ -131,28 +132,42 @@ Cypress.Commands.add('importProject', ({
cy.contains('button', 'OK').click(); cy.contains('button', 'OK').click();
cy.get('.cvat-modal-import-dataset-status').should('be.visible'); cy.get('.cvat-modal-import-dataset-status').should('be.visible');
cy.get('.cvat-notification-notice-import-dataset-start').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'); 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.projectActions(projectName);
cy.get('.cvat-project-actions-menu').contains('Backup Project').click(); 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) => { Cypress.Commands.add('restoreProject', (archiveWithBackup) => {
cy.intercept({ method: /PATCH|POST/, url: /\/api\/projects\/backup.*/ }).as('restoreProject'); cy.intercept({ method: /PATCH|POST/, url: /\/api\/projects\/backup.*/ }).as('restoreProject');
cy.get('.cvat-create-project-dropdown').click(); 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', 202);
cy.wait('@restoreProject').its('response.statusCode').should('equal', 201); 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', 204);
cy.wait('@restoreProject').its('response.statusCode').should('equal', 202); cy.wait('@restoreProject').its('response.statusCode').should('equal', 202);
cy.wait('@restoreProject', { timeout: 5000 }).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.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') .should('exist')
.and('be.visible'); .and('be.visible');
cy.get('[data-icon="close"]').click(); // Close the notification cy.closeNotification('.ant-notification-notice-info');
}); });
Cypress.Commands.add('getDownloadFileName', () => { Cypress.Commands.add('getDownloadFileName', () => {
@ -168,6 +183,7 @@ Cypress.Commands.add('waitForDownload', () => {
cy.getDownloadFileName().then((filename) => { cy.getDownloadFileName().then((filename) => {
cy.verifyDownload(filename); cy.verifyDownload(filename);
}); });
cy.verifyNotification();
}); });
Cypress.Commands.add('deleteProjectViaActions', (projectName) => { Cypress.Commands.add('deleteProjectViaActions', (projectName) => {

Loading…
Cancel
Save