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>)
- 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>)
- 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
- Bumped nuclio version to 1.8.14

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "6.0.2",
"version": "7.0.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts",
"scripts": {

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

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

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

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

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

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier = MIT
@ -389,7 +390,7 @@ export enum CloudStorageCredentialsType {
}
/**
* Task statuses
* Membership roles
* @enum {string}
* @name MembershipRole
* @memberof module:API.cvat.enums
@ -423,3 +424,17 @@ export enum SortingMethod {
PREDEFINED = 'predefined',
RANDOM = 'random',
}
/**
* Types of storage locations
* @enum {string}
* @name StorageLocation
* @memberof module:API.cvat.enums
* @property {string} LOCAL 'local'
* @property {string} CLOUD_STORAGE 'cloud_storage'
* @readonly
*/
export enum StorageLocation {
LOCAL = 'local',
CLOUD_STORAGE = 'cloud_storage',
}

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

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

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

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

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

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

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

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

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) 2022 CVAT.ai Corp
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
return {
__esModule: true,
default: require('../mocks/server-proxy.mock'),
};
});
// Initialize api
window.cvat = require('../../src/api');
const serverProxy = require('../../src/server-proxy');
const serverProxy = require('../../src/server-proxy').default;
// Test cases
describe('Feature: get annotations', () => {

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

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

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

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

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

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

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

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

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

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

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

@ -1,51 +1,130 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { getCore, Storage } from 'cvat-core-wrapper';
const core = getCore();
export enum ExportActionTypes {
OPEN_EXPORT_MODAL = 'OPEN_EXPORT_MODAL',
CLOSE_EXPORT_MODAL = 'CLOSE_EXPORT_MODAL',
OPEN_EXPORT_DATASET_MODAL = 'OPEN_EXPORT_DATASET_MODAL',
CLOSE_EXPORT_DATASET_MODAL = 'CLOSE_EXPORT_DATASET_MODAL',
EXPORT_DATASET = 'EXPORT_DATASET',
EXPORT_DATASET_SUCCESS = 'EXPORT_DATASET_SUCCESS',
EXPORT_DATASET_FAILED = 'EXPORT_DATASET_FAILED',
OPEN_EXPORT_BACKUP_MODAL = 'OPEN_EXPORT_BACKUP_MODAL',
CLOSE_EXPORT_BACKUP_MODAL = 'CLOSE_EXPORT_BACKUP_MODAL',
EXPORT_BACKUP = 'EXPORT_BACKUP',
EXPORT_BACKUP_SUCCESS = 'EXPORT_BACKUP_SUCCESS',
EXPORT_BACKUP_FAILED = 'EXPORT_BACKUP_FAILED',
}
export const exportActions = {
openExportModal: (instance: any) => createAction(ExportActionTypes.OPEN_EXPORT_MODAL, { instance }),
closeExportModal: () => createAction(ExportActionTypes.CLOSE_EXPORT_MODAL),
openExportDatasetModal: (instance: any) => (
createAction(ExportActionTypes.OPEN_EXPORT_DATASET_MODAL, { instance })
),
closeExportDatasetModal: (instance: any) => (
createAction(ExportActionTypes.CLOSE_EXPORT_DATASET_MODAL, { instance })
),
exportDataset: (instance: any, format: string) => (
createAction(ExportActionTypes.EXPORT_DATASET, { instance, format })
),
exportDatasetSuccess: (instance: any, format: string) => (
createAction(ExportActionTypes.EXPORT_DATASET_SUCCESS, { instance, format })
exportDatasetSuccess: (
instance: any,
instanceType: 'project' | 'task' | 'job',
format: string,
isLocal: boolean,
resource: 'Dataset' | 'Annotations',
) => (
createAction(ExportActionTypes.EXPORT_DATASET_SUCCESS, {
instance,
instanceType,
format,
isLocal,
resource,
})
),
exportDatasetFailed: (instance: any, format: string, error: any) => (
exportDatasetFailed: (instance: any, instanceType: 'project' | 'task' | 'job', format: string, error: any) => (
createAction(ExportActionTypes.EXPORT_DATASET_FAILED, {
instance,
instanceType,
format,
error,
})
),
openExportBackupModal: (instance: any) => (
createAction(ExportActionTypes.OPEN_EXPORT_BACKUP_MODAL, { instance })
),
closeExportBackupModal: (instance: any) => (
createAction(ExportActionTypes.CLOSE_EXPORT_BACKUP_MODAL, { instance })
),
exportBackup: (instance: any) => (
createAction(ExportActionTypes.EXPORT_BACKUP, { instance })
),
exportBackupSuccess: (instance: any, instanceType: 'task' | 'project', isLocal: boolean) => (
createAction(ExportActionTypes.EXPORT_BACKUP_SUCCESS, { instance, instanceType, isLocal })
),
exportBackupFailed: (instance: any, instanceType: 'task' | 'project', error: any) => (
createAction(ExportActionTypes.EXPORT_BACKUP_FAILED, { instance, instanceType, error })
),
};
export const exportDatasetAsync = (
instance: any,
format: string,
name: string,
saveImages: boolean,
useDefaultSettings: boolean,
targetStorage: Storage,
name?: string,
): ThunkAction => async (dispatch) => {
dispatch(exportActions.exportDataset(instance, format));
let instanceType: 'project' | 'task' | 'job';
if (instance instanceof core.classes.Project) {
instanceType = 'project';
} else if (instance instanceof core.classes.Task) {
instanceType = 'task';
} else {
instanceType = 'job';
}
try {
const result = await instance.annotations
.exportDataset(format, saveImages, useDefaultSettings, targetStorage, name);
if (result) {
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.href = result;
downloadAnchor.click();
}
const resource = saveImages ? 'Dataset' : 'Annotations';
dispatch(exportActions.exportDatasetSuccess(instance, instanceType, format, !!result, resource));
} catch (error) {
dispatch(exportActions.exportDatasetFailed(instance, instanceType, format, error));
}
};
export const exportBackupAsync = (
instance: any,
targetStorage: Storage,
useDefaultSetting: boolean,
fileName?: string,
): ThunkAction => async (dispatch) => {
dispatch(exportActions.exportBackup(instance));
const instanceType = (instance instanceof core.classes.Project) ? 'project' : 'task';
try {
const url = await instance.annotations.exportDataset(format, saveImages, name);
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.click();
dispatch(exportActions.exportDatasetSuccess(instance, format));
const result = await instance.backup(targetStorage, useDefaultSetting, fileName);
if (result) {
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.href = result;
downloadAnchor.click();
}
dispatch(exportActions.exportBackupSuccess(instance, instanceType, !!result));
} catch (error) {
dispatch(exportActions.exportDatasetFailed(instance, format, error));
dispatch(exportActions.exportBackupFailed(instance, instanceType, error as Error));
}
};

@ -1,58 +1,167 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { createAction, ActionUnion, ThunkAction } from 'utils/redux';
import { CombinedState } from 'reducers';
import { getCore, Storage } from 'cvat-core-wrapper';
import { LogType } from 'cvat-logger';
import { getProjectsAsync } from './projects-actions';
import { jobInfoGenerator, receiveAnnotationsParameters, AnnotationActionTypes } from './annotation-actions';
const core = getCore();
export enum ImportActionTypes {
OPEN_IMPORT_MODAL = 'OPEN_IMPORT_MODAL',
CLOSE_IMPORT_MODAL = 'CLOSE_IMPORT_MODAL',
OPEN_IMPORT_DATASET_MODAL = 'OPEN_IMPORT_DATASET_MODAL',
CLOSE_IMPORT_DATASET_MODAL = 'CLOSE_IMPORT_DATASET_MODAL',
IMPORT_DATASET = 'IMPORT_DATASET',
IMPORT_DATASET_SUCCESS = 'IMPORT_DATASET_SUCCESS',
IMPORT_DATASET_FAILED = 'IMPORT_DATASET_FAILED',
IMPORT_DATASET_UPDATE_STATUS = 'IMPORT_DATASET_UPDATE_STATUS',
OPEN_IMPORT_BACKUP_MODAL = 'OPEN_IMPORT_BACKUP_MODAL',
CLOSE_IMPORT_BACKUP_MODAL = 'CLOSE_IMPORT_BACKUP_MODAL',
IMPORT_BACKUP = 'IMPORT_BACKUP',
IMPORT_BACKUP_SUCCESS = 'IMPORT_BACKUP_SUCCESS',
IMPORT_BACKUP_FAILED = 'IMPORT_BACKUP_FAILED',
}
export const importActions = {
openImportModal: (instance: any) => createAction(ImportActionTypes.OPEN_IMPORT_MODAL, { instance }),
closeImportModal: () => createAction(ImportActionTypes.CLOSE_IMPORT_MODAL),
importDataset: (projectId: number) => (
createAction(ImportActionTypes.IMPORT_DATASET, { id: projectId })
openImportDatasetModal: (instance: any) => (
createAction(ImportActionTypes.OPEN_IMPORT_DATASET_MODAL, { instance })
),
closeImportDatasetModal: (instance: any) => (
createAction(ImportActionTypes.CLOSE_IMPORT_DATASET_MODAL, { instance })
),
importDataset: (instance: any, format: string) => (
createAction(ImportActionTypes.IMPORT_DATASET, { instance, format })
),
importDatasetSuccess: () => (
createAction(ImportActionTypes.IMPORT_DATASET_SUCCESS)
importDatasetSuccess: (instance: any, resource: 'dataset' | 'annotation') => (
createAction(ImportActionTypes.IMPORT_DATASET_SUCCESS, { instance, resource })
),
importDatasetFailed: (instance: any, error: any) => (
importDatasetFailed: (instance: any, resource: 'dataset' | 'annotation', error: any) => (
createAction(ImportActionTypes.IMPORT_DATASET_FAILED, {
instance,
resource,
error,
})
),
importDatasetUpdateStatus: (progress: number, status: string) => (
createAction(ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS, { progress, status })
importDatasetUpdateStatus: (instance: any, progress: number, status: string) => (
createAction(ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS, { instance, progress, status })
),
openImportBackupModal: (instanceType: 'project' | 'task') => (
createAction(ImportActionTypes.OPEN_IMPORT_BACKUP_MODAL, { instanceType })
),
closeImportBackupModal: (instanceType: 'project' | 'task') => (
createAction(ImportActionTypes.CLOSE_IMPORT_BACKUP_MODAL, { instanceType })
),
importBackup: () => createAction(ImportActionTypes.IMPORT_BACKUP),
importBackupSuccess: (instanceId: number, instanceType: 'project' | 'task') => (
createAction(ImportActionTypes.IMPORT_BACKUP_SUCCESS, { instanceId, instanceType })
),
importBackupFailed: (instanceType: 'project' | 'task', error: any) => (
createAction(ImportActionTypes.IMPORT_BACKUP_FAILED, { instanceType, error })
),
};
export const importDatasetAsync = (instance: any, format: string, file: File): ThunkAction => (
export const importDatasetAsync = (
instance: any,
format: string,
useDefaultSettings: boolean,
sourceStorage: Storage,
file: File | string,
): ThunkAction => (
async (dispatch, getState) => {
const resource = instance instanceof core.classes.Project ? 'dataset' : 'annotation';
try {
const state: CombinedState = getState();
if (state.import.importingId !== null) {
throw Error('Only one importing of dataset allowed at the same time');
if (instance instanceof core.classes.Project) {
if (state.import.projects.dataset.current?.[instance.id]) {
throw Error('Only one importing of annotation/dataset allowed at the same time');
}
dispatch(importActions.importDataset(instance, format));
await instance.annotations
.importDataset(format, useDefaultSettings, sourceStorage, file,
(message: string, progress: number) => (
dispatch(importActions.importDatasetUpdateStatus(
instance, Math.floor(progress * 100), message,
))
));
} else if (instance instanceof core.classes.Task) {
if (state.import.tasks.dataset.current?.[instance.id]) {
throw Error('Only one importing of annotation/dataset allowed at the same time');
}
dispatch(importActions.importDataset(instance, format));
await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file);
} else { // job
if (state.import.tasks.dataset.current?.[instance.taskId]) {
throw Error('Annotations is being uploaded for the task');
}
if (state.import.jobs.dataset.current?.[instance.id]) {
throw Error('Only one uploading of annotations for a job allowed at the same time');
}
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
dispatch(importActions.importDataset(instance, format));
const frame = state.annotation.player.frame.number;
await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file);
await instance.logger.log(LogType.uploadAnnotations, {
...(await jobInfoGenerator(instance)),
});
await instance.annotations.clear(true);
await instance.actions.clear();
const history = await instance.actions.get();
// One more update to escape some problems
// in canvas when shape with the same
// clientID has different type (polygon, rectangle) for example
dispatch({
type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS,
payload: {
states: [],
history,
},
});
const states = await instance.annotations.get(frame, showAllInterpolationTracks, filters);
setTimeout(() => {
dispatch({
type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS,
payload: {
history,
states,
},
});
});
}
dispatch(importActions.importDataset(instance.id));
await instance.annotations.importDataset(format, file, (message: string, progress: number) => (
dispatch(importActions.importDatasetUpdateStatus(Math.floor(progress * 100), message))
));
} catch (error) {
dispatch(importActions.importDatasetFailed(instance, error));
dispatch(importActions.importDatasetFailed(instance, resource, error));
return;
}
dispatch(importActions.importDatasetSuccess());
dispatch(getProjectsAsync({ id: instance.id }, getState().projects.tasksGettingQuery));
dispatch(importActions.importDatasetSuccess(instance, resource));
if (instance instanceof core.classes.Project) {
dispatch(getProjectsAsync({ id: instance.id }, getState().projects.tasksGettingQuery));
}
}
);
export const importBackupAsync = (instanceType: 'project' | 'task', storage: Storage, file: File | string): ThunkAction => (
async (dispatch) => {
dispatch(importActions.importBackup());
try {
const inctanceClass = (instanceType === 'task') ? core.classes.Task : core.classes.Project;
const instance = await inctanceClass.restore(storage, file);
dispatch(importActions.importBackupSuccess(instance.id, instanceType));
} catch (error) {
dispatch(importActions.importBackupFailed(instanceType, error));
}
}
);

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -28,12 +29,6 @@ export enum ProjectsActionTypes {
DELETE_PROJECT = 'DELETE_PROJECT',
DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS',
DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED',
BACKUP_PROJECT = 'BACKUP_PROJECT',
BACKUP_PROJECT_SUCCESS = 'BACKUP_PROJECT_SUCCESS',
BACKUP_PROJECT_FAILED = 'BACKUP_PROJECT_FAILED',
RESTORE_PROJECT = 'IMPORT_PROJECT',
RESTORE_PROJECT_SUCCESS = 'IMPORT_PROJECT_SUCCESS',
RESTORE_PROJECT_FAILED = 'IMPORT_PROJECT_FAILED',
}
// prettier-ignore
@ -63,20 +58,6 @@ const projectActions = {
deleteProjectFailed: (projectId: number, error: any) => (
createAction(ProjectsActionTypes.DELETE_PROJECT_FAILED, { projectId, error })
),
backupProject: (projectId: number) => createAction(ProjectsActionTypes.BACKUP_PROJECT, { projectId }),
backupProjectSuccess: (projectID: number) => (
createAction(ProjectsActionTypes.BACKUP_PROJECT_SUCCESS, { projectID })
),
backupProjectFailed: (projectID: number, error: any) => (
createAction(ProjectsActionTypes.BACKUP_PROJECT_FAILED, { projectId: projectID, error })
),
restoreProject: () => createAction(ProjectsActionTypes.RESTORE_PROJECT),
restoreProjectSuccess: (projectID: number) => (
createAction(ProjectsActionTypes.RESTORE_PROJECT_SUCCESS, { projectID })
),
restoreProjectFailed: (error: any) => (
createAction(ProjectsActionTypes.RESTORE_PROJECT_FAILED, { error })
),
};
export type ProjectActions = ActionUnion<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) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { TasksQuery, CombinedState, Indexable } from 'reducers';
import { getCVATStore } from 'cvat-store';
import { getCore } from 'cvat-core-wrapper';
import {
TasksQuery, CombinedState, Indexable, StorageLocation,
} from 'reducers';
import { getCore, Storage } from 'cvat-core-wrapper';
import { getInferenceStatusAsync } from './models-actions';
const cvat = getCore();
@ -15,9 +17,6 @@ export enum TasksActionTypes {
GET_TASKS = 'GET_TASKS',
GET_TASKS_SUCCESS = 'GET_TASKS_SUCCESS',
GET_TASKS_FAILED = 'GET_TASKS_FAILED',
LOAD_ANNOTATIONS = 'LOAD_ANNOTATIONS',
LOAD_ANNOTATIONS_SUCCESS = 'LOAD_ANNOTATIONS_SUCCESS',
LOAD_ANNOTATIONS_FAILED = 'LOAD_ANNOTATIONS_FAILED',
DELETE_TASK = 'DELETE_TASK',
DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS',
DELETE_TASK_FAILED = 'DELETE_TASK_FAILED',
@ -32,12 +31,6 @@ export enum TasksActionTypes {
UPDATE_JOB_SUCCESS = 'UPDATE_JOB_SUCCESS',
UPDATE_JOB_FAILED = 'UPDATE_JOB_FAILED',
HIDE_EMPTY_TASKS = 'HIDE_EMPTY_TASKS',
EXPORT_TASK = 'EXPORT_TASK',
EXPORT_TASK_SUCCESS = 'EXPORT_TASK_SUCCESS',
EXPORT_TASK_FAILED = 'EXPORT_TASK_FAILED',
IMPORT_TASK = 'IMPORT_TASK',
IMPORT_TASK_SUCCESS = 'IMPORT_TASK_SUCCESS',
IMPORT_TASK_FAILED = 'IMPORT_TASK_FAILED',
SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE',
}
@ -103,157 +96,6 @@ export function getTasksAsync(query: TasksQuery, updateQuery = true): ThunkActio
};
}
function loadAnnotations(task: any, loader: any): AnyAction {
const action = {
type: TasksActionTypes.LOAD_ANNOTATIONS,
payload: {
task,
loader,
},
};
return action;
}
function loadAnnotationsSuccess(task: any): AnyAction {
const action = {
type: TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS,
payload: {
task,
},
};
return action;
}
function loadAnnotationsFailed(task: any, error: any): AnyAction {
const action = {
type: TasksActionTypes.LOAD_ANNOTATIONS_FAILED,
payload: {
task,
error,
},
};
return action;
}
export function loadAnnotationsAsync(
task: any,
loader: any,
file: File,
): ThunkAction<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 {
const action = {
type: TasksActionTypes.DELETE_TASK,
@ -353,6 +195,8 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
use_zip_chunks: data.advanced.useZipChunks,
use_cache: data.advanced.useCache,
sorting_method: data.advanced.sortingMethod,
source_storage: new Storage(data.advanced.sourceStorage || { location: StorageLocation.LOCAL }).toJSON(),
target_storage: new Storage(data.advanced.targetStorage || { location: StorageLocation.LOCAL }).toJSON(),
};
if (data.projectId) {

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -9,8 +10,6 @@ import Modal from 'antd/lib/modal';
import { LoadingOutlined } from '@ant-design/icons';
// eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface';
import LoadSubmenu from './load-submenu';
import { DimensionType } from '../../reducers';
interface Props {
@ -19,12 +18,10 @@ interface Props {
bugTracker: string;
loaders: any[];
dumpers: any[];
loadActivity: string | null;
inferenceIsActive: boolean;
taskDimension: DimensionType;
backupIsActive: boolean;
onClickMenu: (params: MenuInfo) => void;
onUploadAnnotations: (format: string, file: File) => void;
exportIsActive: boolean;
}
export enum Actions {
@ -34,7 +31,7 @@ export enum Actions {
RUN_AUTO_ANNOTATION = 'run_auto_annotation',
MOVE_TASK_TO_PROJECT = 'move_task_to_project',
OPEN_BUG_TRACKER = 'open_bug_tracker',
EXPORT_TASK = 'export_task',
BACKUP_TASK = 'backup_task',
}
function ActionsMenuComponent(props: Props): JSX.Element {
@ -42,12 +39,8 @@ function ActionsMenuComponent(props: Props): JSX.Element {
taskID,
bugTracker,
inferenceIsActive,
loaders,
backupIsActive,
onClickMenu,
onUploadAnnotations,
loadActivity,
taskDimension,
exportIsActive,
} = props;
const onClickMenuWrapper = useCallback(
@ -79,38 +72,16 @@ function ActionsMenuComponent(props: Props): JSX.Element {
return (
<Menu selectable={false} className='cvat-actions-menu' onClick={onClickMenuWrapper}>
{LoadSubmenu({
loaders,
loadActivity,
onFileUpload: (format: string, file: File): void => {
if (file) {
Modal.confirm({
title: 'Current annotation will be lost',
content: 'You are going to upload new annotations to this task. Continue?',
className: 'cvat-modal-content-load-task-annotation',
onOk: () => {
onUploadAnnotations(format, file);
},
okButtonProps: {
type: 'primary',
danger: true,
},
okText: 'Update',
});
}
},
menuKey: Actions.LOAD_TASK_ANNO,
taskDimension,
})}
<Menu.Item key={Actions.LOAD_TASK_ANNO}>Upload annotations</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>}
<Menu.Item disabled={inferenceIsActive} key={Actions.RUN_AUTO_ANNOTATION}>
Automatic annotation
</Menu.Item>
<Menu.Item
key={Actions.EXPORT_TASK}
disabled={exportIsActive}
icon={exportIsActive && <LoadingOutlined id='cvat-export-task-loading' />}
key={Actions.BACKUP_TASK}
disabled={backupIsActive}
icon={backupIsActive && <LoadingOutlined id='cvat-backup-task-loading' />}
>
Backup Task
</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) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -17,7 +18,6 @@
}
}
.cvat-menu-load-submenu-item,
.cvat-menu-dump-submenu-item,
.cvat-menu-export-submenu-item {
> span[role='img'] {
@ -29,28 +29,12 @@
}
}
.ant-menu-item.cvat-menu-load-submenu-item {
margin: 0;
padding: 0;
> span > .ant-upload {
width: 100%;
height: 100%;
> span > button {
width: 100%;
height: 100%;
text-align: left;
}
}
}
.cvat-menu-icon {
font-size: 16px;
margin-left: 8px;
align-self: center;
}
#cvat-export-task-loading {
#cvat-backup-task-loading {
margin-left: 10;
}

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

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -12,17 +13,47 @@ import Select from 'antd/lib/select';
import { Col, Row } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import Form, { FormInstance } from 'antd/lib/form';
import Collapse from 'antd/lib/collapse';
import Button from 'antd/lib/button';
import Input from 'antd/lib/input';
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 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';
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(
{ formRef, inputRef }:
{ 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 (
<Form layout='vertical' ref={formRef}>
<Form layout='vertical' ref={formRef} initialValues={initialValues}>
<Form.Item
name='bug_tracker'
label='Issue tracker'
@ -121,12 +159,32 @@ function AdvancedConfigurationForm({ formRef }: { formRef: RefObject<FormInstanc
>
<Input />
</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>
);
}
export default function CreateProjectContent(): JSX.Element {
const [projectLabels, setProjectLabels] = useState<any[]>([]);
const [sourceStorageLocation, setSourceStorageLocation] = useState(StorageLocation.LOCAL);
const [targetStorageLocation, setTargetStorageLocation] = useState(StorageLocation.LOCAL);
const nameFormRef = useRef<FormInstance>(null);
const nameInputRef = useRef<Input>(null);
const adaptiveAutoAnnotationFormRef = useRef<FormInstance>(null);
@ -140,23 +198,32 @@ export default function CreateProjectContent(): JSX.Element {
if (nameFormRef.current) nameFormRef.current.resetFields();
if (advancedFormRef.current) advancedFormRef.current.resetFields();
setProjectLabels([]);
setSourceStorageLocation(StorageLocation.LOCAL);
setTargetStorageLocation(StorageLocation.LOCAL);
};
const focusForm = (): void => {
nameInputRef.current?.focus();
};
const sumbit = async (): Promise<any> => {
const submit = async (): Promise<any> => {
try {
let projectData: Record<string, any> = {};
if (nameFormRef.current && advancedFormRef.current) {
if (nameFormRef.current) {
const basicValues = await nameFormRef.current.validateFields();
const advancedValues = await advancedFormRef.current.validateFields();
const advancedValues = advancedFormRef.current ? await advancedFormRef.current.validateFields() : {};
const adaptiveAutoAnnotationValues = await adaptiveAutoAnnotationFormRef.current?.validateFields();
projectData = {
...projectData,
...advancedValues,
name: basicValues.name,
source_storage: new Storage(
advancedValues.sourceStorage || { location: StorageLocation.LOCAL },
).toJSON(),
target_storage: new Storage(
advancedValues.targetStorage || { location: StorageLocation.LOCAL },
).toJSON(),
};
if (adaptiveAutoAnnotationValues) {
@ -174,14 +241,14 @@ export default function CreateProjectContent(): JSX.Element {
};
const onSubmitAndOpen = async (): Promise<void> => {
const createdProject = await sumbit();
const createdProject = await submit();
if (createdProject) {
history.push(`/projects/${createdProject.id}`);
}
};
const onSubmitAndContinue = async (): Promise<void> => {
const res = await sumbit();
const res = await submit();
if (res) {
resetForm();
notification.info({
@ -216,7 +283,17 @@ export default function CreateProjectContent(): JSX.Element {
/>
</Col>
<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 span={24}>
<Row justify='end' gutter={5}>

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

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

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

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -24,6 +25,9 @@ import GlobalErrorBoundary from 'components/global-error-boundary/global-error-b
import ShortcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog';
import ExportDatasetModal from 'components/export-dataset/export-dataset-modal';
import ExportBackupModal from 'components/export-backup/export-backup-modal';
import ImportDatasetModal from 'components/import-dataset/import-dataset-modal';
import ImportBackupModal from 'components/import-backup/import-backup-modal';
import ModelsPageContainer from 'containers/models-page/models-page';
import JobsPageComponent from 'components/jobs-page/jobs-page';
@ -399,6 +403,9 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
</GlobalHotKeys>
{/* eslint-disable-next-line */}
<ExportDatasetModal />
<ExportBackupModal />
<ImportDatasetModal />
<ImportBackupModal />
{/* eslint-disable-next-line */}
<a id='downloadAnchor' target='_blank' style={{ display: 'none' }} download />
</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) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useState, useEffect, useCallback } from 'react';
import { connect, useDispatch } from 'react-redux';
import Modal from 'antd/lib/modal';
import Notification from 'antd/lib/notification';
import { useSelector, useDispatch } from 'react-redux';
import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text';
import Select from 'antd/lib/select';
import Checkbox from 'antd/lib/checkbox';
import Input from 'antd/lib/input';
import Form from 'antd/lib/form';
import { CombinedState } from 'reducers';
import Switch from 'antd/lib/switch';
import Space from 'antd/lib/space';
import TargetStorageField from 'components/storage/target-storage-field';
import { CombinedState, StorageLocation } from 'reducers';
import { exportActions, exportDatasetAsync } from 'actions/export-actions';
import { getCore } from 'cvat-core-wrapper';
import { getCore, Storage, StorageData } from 'cvat-core-wrapper';
const core = getCore();
@ -24,43 +26,94 @@ type FormValues = {
selectedFormat: string | undefined;
saveImages: boolean;
customName: string | undefined;
targetStorage: StorageData;
useProjectTargetStorage: boolean;
};
const initialValues: FormValues = {
selectedFormat: undefined,
saveImages: false,
customName: undefined,
targetStorage: {
location: StorageLocation.LOCAL,
cloudStorageId: undefined,
},
useProjectTargetStorage: true,
};
function ExportDatasetModal(): JSX.Element {
function ExportDatasetModal(props: StateToProps): JSX.Element {
const {
dumpers,
instance,
current,
} = props;
const [instanceType, setInstanceType] = useState('');
const [activities, setActivities] = useState<string[]>([]);
const [useDefaultTargetStorage, setUseDefaultTargetStorage] = useState(true);
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 instance = useSelector((state: CombinedState) => state.export.instance);
const modalVisible = useSelector((state: CombinedState) => state.export.modalVisible);
const dumpers = useSelector((state: CombinedState) => state.formats.annotationFormats.dumpers);
const { tasks: taskExportActivities, projects: projectExportActivities } = useSelector(
(state: CombinedState) => state.export,
);
const initActivities = (): void => {
useEffect(() => {
if (instance instanceof core.classes.Project) {
setInstanceType(`project #${instance.id}`);
setActivities(projectExportActivities[instance.id] || []);
} else if (instance) {
const taskID = instance instanceof core.classes.Task ? instance.id : instance.taskId;
setInstanceType(`task #${taskID}`);
setActivities(taskExportActivities[taskID] || []);
} else if (instance instanceof core.classes.Task || instance instanceof core.classes.Job) {
if (instance instanceof core.classes.Task) {
setInstanceType(`task #${instance.id}`);
} else {
setInstanceType(`job #${instance.id}`);
}
if (instance.mode === 'interpolation' && instance.dimension === '2d') {
form.setFieldsValue({ selectedFormat: 'CVAT for video 1.1' });
} else if (instance.mode === 'annotation' && instance.dimension === '2d') {
form.setFieldsValue({ selectedFormat: 'CVAT for images 1.1' });
}
}
};
}, [instance]);
useEffect(() => {
initActivities();
}, [instance?.id, instance instanceof core.classes.Project, taskExportActivities, projectExportActivities]);
if (instance) {
if (instance instanceof core.classes.Project || instance instanceof core.classes.Task) {
setDefaultStorageLocation(instance.targetStorage?.location || StorageLocation.LOCAL);
setDefaultStorageCloudId(instance.targetStorage?.cloudStorageId || null);
} else {
core.tasks.get({ id: instance.taskId })
.then((response: any) => {
if (response.length) {
const [taskInstance] = response;
setDefaultStorageLocation(taskInstance.targetStorage?.location || StorageLocation.LOCAL);
setDefaultStorageCloudId(taskInstance.targetStorage?.cloudStorageId || null);
}
})
.catch((error: Error) => {
if ((error as any).code !== 403) {
Notification.error({
message: `Could not fetch the task ${instance.taskId}`,
description: error.toString(),
});
}
});
}
}
}, [instance]);
useEffect(() => {
// eslint-disable-next-line prefer-template
setHelpMessage(`Export to ${(defaultStorageLocation) ? defaultStorageLocation.split('_')[0] : 'local'} ` +
`storage ${(defaultStorageCloudId) ? `${defaultStorageCloudId}` : ''}`);
}, [defaultStorageLocation, defaultStorageCloudId]);
const closeModal = (): void => {
setUseDefaultTargetStorage(true);
setTargetStorage({ location: StorageLocation.LOCAL });
form.resetFields();
dispatch(exportActions.closeExportModal());
dispatch(exportActions.closeExportDatasetModal(instance));
};
const handleExport = useCallback(
@ -70,26 +123,32 @@ function ExportDatasetModal(): JSX.Element {
exportDatasetAsync(
instance,
values.selectedFormat as string,
values.customName ? `${values.customName}.zip` : '',
values.saveImages,
useDefaultTargetStorage,
useDefaultTargetStorage ? new Storage({
location: defaultStorageLocation,
cloudStorageId: defaultStorageCloudId,
}) : new Storage(targetStorage),
values.customName ? `${values.customName}.zip` : null,
),
);
closeModal();
const resource = values.saveImages ? 'Dataset' : 'Annotations';
Notification.info({
message: 'Dataset export started',
message: `${resource} export started`,
description:
`Dataset export was started for ${instanceType}. ` +
'Download will start automatically as soon as the dataset is ready.',
`${resource} export was started for ${instanceType}. ` +
`Download will start automatically as soon as the ${resource} is ready.`,
className: `cvat-notification-notice-export-${instanceType.split(' ')[0]}-start`,
});
},
[instance, instanceType],
[instance, instanceType, useDefaultTargetStorage, defaultStorageLocation, defaultStorageCloudId, targetStorage],
);
return (
<Modal
title={`Export ${instanceType} as a dataset`}
visible={modalVisible}
title={<Text strong>{`Export ${instanceType} as a dataset`}</Text>}
visible={!!instance}
onCancel={closeModal}
onOk={() => form.submit()}
className={`cvat-modal-export-${instanceType.split(' ')[0]}`}
@ -98,20 +157,13 @@ function ExportDatasetModal(): JSX.Element {
<Form
name='Export dataset'
form={form}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
initialValues={
{
selectedFormat: undefined,
saveImages: false,
customName: undefined,
} as FormValues
}
layout='vertical'
initialValues={initialValues}
onFinish={handleExport}
>
<Form.Item
name='selectedFormat'
label='Export format'
label={<Text strong>Export format</Text>}
rules={[{ required: true, message: 'Format must be selected' }]}
>
<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)
.map(
(dumper: any): JSX.Element => {
const pending = (activities || []).includes(dumper.name);
const pending = (instance && current ? current : [])
.includes(dumper.name);
const disabled = !dumper.enabled || pending;
return (
<Select.Option
@ -138,19 +191,57 @@ function ExportDatasetModal(): JSX.Element {
)}
</Select>
</Form.Item>
<Form.Item name='saveImages' valuePropName='checked' wrapperCol={{ offset: 8, span: 16 }}>
<Checkbox>Save images</Checkbox>
</Form.Item>
<Form.Item label='Custom name' name='customName'>
<Space>
<Form.Item name='saveImages' className='cvat-modal-export-switch-use-default-storage'>
<Switch className='cvat-modal-export-save-images' />
</Form.Item>
<Text strong>Save images</Text>
</Space>
<Form.Item label={<Text strong>Custom name</Text>} name='customName'>
<Input
placeholder='Custom name for a dataset'
suffix='.zip'
className='cvat-modal-export-filename-input'
/>
</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>
</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;
}
}
.cvat-modal-export-switch-use-default-storage {
display: table-cell;
}

@ -1,20 +1,14 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect, useState } from 'react';
import Form from 'antd/lib/form';
import notification from 'antd/lib/notification';
import AutoComplete from 'antd/lib/auto-complete';
import Input from 'antd/lib/input';
import { debounce } from 'lodash';
import Select from 'antd/lib/select';
import { getCore } from 'cvat-core-wrapper';
import { CloudStorage } from 'reducers';
import { AzureProvider, GoogleCloudProvider, S3Provider } from 'icons';
import { ProviderType } from 'utils/enums';
import SelectCloudStorage from 'components/select-cloud-storage/select-cloud-storage';
import CloudStorageFiles from './cloud-storages-files';
interface Props {
@ -27,61 +21,15 @@ interface Props {
onSelectCloudStorage: (cloudStorageId: number | null) => void;
}
async function searchCloudStorages(filter: Record<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 searchCloudStoragesWrapper = debounce((phrase, setList) => {
const filter = {
filter: JSON.stringify({
and: [{
'==': [{ var: 'display_name' }, phrase],
}],
}),
};
searchCloudStorages(filter).then((list) => {
setList(list);
});
}, 500);
export default function CloudStorageTab(props: Props): JSX.Element {
const { searchPhrase, setSearchPhrase } = props;
const [initialList, setInitialList] = useState<CloudStorage[]>([]);
const [list, setList] = useState<CloudStorage[]>([]);
const {
formRef, cloudStorage, selectedFiles, onSelectFiles, onSelectCloudStorage,
} = props;
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(() => {
if (cloudStorage) {
setSelectedManifest(cloudStorage.manifests[0]);
@ -94,67 +42,15 @@ export default function CloudStorageTab(props: Props): JSX.Element {
}
}, [selectedManifest]);
const onBlur = (): void => {
if (!searchPhrase && cloudStorage) {
onSelectCloudStorage(null);
} else if (searchPhrase) {
const potentialStorages = list.filter((_cloudStorage) => _cloudStorage.displayName.includes(searchPhrase));
if (potentialStorages.length === 1) {
const potentialStorage = potentialStorages[0];
setSearchPhrase(potentialStorage.displayName);
// eslint-disable-next-line prefer-destructuring
potentialStorage.manifestPath = potentialStorage.manifests[0];
onSelectCloudStorage(potentialStorage);
}
}
};
return (
<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 ? (
<Form.Item
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;
}
.cvat-modal-import-switch-use-default-storage {
display: table-cell;
}
.cvat-modal-import-dataset-status .ant-modal-body {
display: flex;
align-items: center;

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,58 +1,223 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { omit } from 'lodash';
import { ImportActions, ImportActionTypes } from 'actions/import-actions';
import { getCore } from 'cvat-core-wrapper';
import { ImportState } from '.';
const core = getCore();
const defaultProgress = 0.0;
export function defineActititiesField(instance: any): 'projects' | 'tasks' | 'jobs' {
if (instance instanceof core.classes.Project) {
return 'projects';
}
if (instance instanceof core.classes.Task) {
return 'tasks';
}
return 'jobs';
}
const defaultState: ImportState = {
progress: 0.0,
status: '',
instance: null,
importingId: null,
modalVisible: false,
projects: {
dataset: {
modalInstance: null,
current: {},
},
backup: {
modalVisible: false,
importing: false,
},
},
tasks: {
dataset: {
modalInstance: null,
current: {},
},
backup: {
modalVisible: false,
importing: false,
},
},
jobs: {
dataset: {
modalInstance: null,
current: {},
},
},
instanceType: null,
};
export default (state: ImportState = defaultState, action: ImportActions): ImportState => {
switch (action.type) {
case ImportActionTypes.OPEN_IMPORT_MODAL:
case ImportActionTypes.OPEN_IMPORT_DATASET_MODAL: {
const { instance } = action.payload;
const activitiesField = defineActititiesField(instance);
return {
...state,
modalVisible: true,
instance: action.payload.instance,
[activitiesField]: {
...state[activitiesField],
dataset: {
...state[activitiesField].dataset,
modalInstance: instance,
},
},
instanceType: activitiesField
.slice(0, activitiesField.length - 1) as 'project' | 'task' | 'job',
};
case ImportActionTypes.CLOSE_IMPORT_MODAL: {
}
case ImportActionTypes.CLOSE_IMPORT_DATASET_MODAL: {
const { instance } = action.payload;
const activitiesField = defineActititiesField(instance);
return {
...state,
modalVisible: false,
instance: null,
[activitiesField]: {
...state[activitiesField],
dataset: {
...state[activitiesField].dataset,
modalInstance: null,
},
},
instanceType: null,
};
}
case ImportActionTypes.IMPORT_DATASET: {
const { id } = action.payload;
const { format, instance } = action.payload;
const activitiesField = defineActititiesField(instance);
let updatedActivity: {
format: string;
status?: string;
progress?: number;
} = { format };
if (activitiesField === 'projects') {
updatedActivity = {
...updatedActivity,
status: 'The file is being uploaded to the server',
progress: defaultProgress,
};
}
return {
...state,
importingId: id,
status: 'The file is being uploaded to the server',
[activitiesField]: {
...state[activitiesField],
dataset: {
...state[activitiesField].dataset,
current: {
...state[activitiesField].dataset.current,
[instance.id]: updatedActivity,
},
},
},
};
}
case ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS: {
const { progress, status } = action.payload;
const { progress, status, instance } = action.payload;
const activitiesField = defineActititiesField(instance);
return {
...state,
progress,
status,
[activitiesField]: {
...state[activitiesField],
dataset: {
...state[activitiesField].dataset,
current: {
...state[activitiesField].dataset.current,
[instance.id]: {
...state[activitiesField].dataset.current[instance.id] as Record<string, unknown>,
progress,
status,
},
},
},
},
};
}
case ImportActionTypes.IMPORT_DATASET_FAILED:
case ImportActionTypes.IMPORT_DATASET_SUCCESS: {
const { instance } = action.payload;
const activitiesField = defineActititiesField(instance);
const { current } = state[activitiesField].dataset;
return {
...state,
[activitiesField]: {
...state[activitiesField],
dataset: {
...state[activitiesField].dataset,
current: omit(current, instance.id),
},
},
};
}
case ImportActionTypes.OPEN_IMPORT_BACKUP_MODAL: {
const { instanceType } = action.payload;
const field = `${instanceType}s` as 'projects' | 'tasks';
return {
...state,
[field]: {
...state[field],
backup: {
modalVisible: true,
importing: false,
},
},
instanceType,
};
}
case ImportActionTypes.CLOSE_IMPORT_BACKUP_MODAL: {
const { instanceType } = action.payload;
const field = `${instanceType}s` as 'projects' | 'tasks';
return {
...state,
[field]: {
...state[field],
backup: {
...state[field].backup,
modalVisible: false,
},
},
instanceType: null,
};
}
case ImportActionTypes.IMPORT_BACKUP: {
const { instanceType } = state;
const field = `${instanceType}s` as 'projects' | 'tasks';
return {
...state,
[field]: {
...state[field],
backup: {
...state[field].backup,
importing: true,
},
},
};
}
case ImportActionTypes.IMPORT_BACKUP_FAILED:
case ImportActionTypes.IMPORT_BACKUP_SUCCESS: {
const { instanceType } = action.payload;
const field = `${instanceType}s` as 'projects' | 'tasks';
return {
...state,
progress: defaultState.progress,
status: defaultState.status,
importingId: null,
[`${instanceType}s`]: {
...state[field],
backup: {
...state[field].backup,
importing: false,
},
},
};
}
default:

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

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

@ -1,9 +1,9 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { AnyAction } from 'redux';
import { omit } from 'lodash';
import { ProjectsActionTypes } from 'actions/projects-actions';
import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { AuthActionTypes } from 'actions/auth-actions';
@ -37,9 +37,7 @@ const defaultState: ProjectsState = {
id: null,
error: '',
},
backups: {},
},
restoring: false,
};
export default (state: ProjectsState = defaultState, action: AnyAction): ProjectsState => {
@ -204,48 +202,6 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project
},
};
}
case ProjectsActionTypes.BACKUP_PROJECT: {
const { projectId } = action.payload;
const { backups } = state.activities;
return {
...state,
activities: {
...state.activities,
backups: {
...backups,
...Object.fromEntries([[projectId, true]]),
},
},
};
}
case ProjectsActionTypes.BACKUP_PROJECT_FAILED:
case ProjectsActionTypes.BACKUP_PROJECT_SUCCESS: {
const { projectID } = action.payload;
const { backups } = state.activities;
return {
...state,
activities: {
...state.activities,
backups: omit(backups, [projectID]),
},
};
}
case ProjectsActionTypes.RESTORE_PROJECT: {
return {
...state,
restoring: true,
};
}
case ProjectsActionTypes.RESTORE_PROJECT_FAILED:
case ProjectsActionTypes.RESTORE_PROJECT_SUCCESS: {
return {
...state,
restoring: false,
};
}
case BoundariesActionTypes.RESET_AFTER_ERROR:
case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState };

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

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -31,17 +32,14 @@ const defaultState: TasksState = {
projectId: null,
},
activities: {
loads: {},
deletes: {},
creates: {
taskId: null,
status: '',
error: '',
},
backups: {},
jobUpdates: {},
},
importing: false,
};
export default (state: TasksState = defaultState, action: AnyAction): TasksState => {
@ -82,40 +80,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
initialized: true,
fetching: false,
};
case TasksActionTypes.LOAD_ANNOTATIONS: {
const { task } = action.payload;
const { loader } = action.payload;
const { loads } = state.activities;
loads[task.id] = task.id in loads ? loads[task.id] : loader.name;
return {
...state,
activities: {
...state.activities,
loads: {
...loads,
},
},
};
}
case TasksActionTypes.LOAD_ANNOTATIONS_FAILED:
case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: {
const { task } = action.payload;
const { loads } = state.activities;
delete loads[task.id];
return {
...state,
activities: {
...state.activities,
loads: {
...loads,
},
},
};
}
case TasksActionTypes.DELETE_TASK: {
const { taskID } = action.payload;
const { deletes } = state.activities;
@ -164,49 +128,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
},
};
}
case TasksActionTypes.EXPORT_TASK: {
const { taskID } = action.payload;
const { backups } = state.activities;
return {
...state,
activities: {
...state.activities,
backups: {
...backups,
...Object.fromEntries([[taskID, true]]),
},
},
};
}
case TasksActionTypes.EXPORT_TASK_FAILED:
case TasksActionTypes.EXPORT_TASK_SUCCESS: {
const { taskID } = action.payload;
const { backups } = state.activities;
delete backups[taskID];
return {
...state,
activities: {
...state.activities,
backups: omit(backups, [taskID]),
},
};
}
case TasksActionTypes.IMPORT_TASK: {
return {
...state,
importing: true,
};
}
case TasksActionTypes.IMPORT_TASK_FAILED:
case TasksActionTypes.IMPORT_TASK_SUCCESS: {
return {
...state,
importing: false,
};
}
case TasksActionTypes.CREATE_TASK: {
return {
...state,

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

@ -648,3 +648,11 @@ def db_storage_to_storage_instance(db_storage):
'specific_attributes': db_storage.get_specific_attributes()
}
return get_cloud_storage_instance(cloud_provider=db_storage.provider_type, **details)
@validate_bucket_status
def import_from_cloud_storage(storage, file_name):
return storage.download_fileobj(file_name)
@validate_bucket_status
def export_to_cloud_storage(storage, file_path, file_name):
storage.upload_file(file_path, file_name)

@ -279,6 +279,11 @@ class AnnotationMixin:
return Response(serializer.data)
def import_annotations(self, request, pk, db_obj, import_func, rq_func, rq_id):
is_tus_request = request.headers.get('Upload-Length', None) is not None or \
request.method == 'OPTIONS'
if is_tus_request:
return self.init_tus_upload(request)
use_default_location = request.query_params.get('use_default_location', True)
use_settings = strtobool(str(use_default_location))
obj = db_obj if use_settings else request.query_params

@ -108,3 +108,28 @@ def parse_specific_attributes(specific_attributes):
return {
key: value for (key, value) in parsed_specific_attributes
} if parsed_specific_attributes else dict()
def process_failed_job(rq_job):
if rq_job.meta['tmp_file_descriptor']:
os.close(rq_job.meta['tmp_file_descriptor'])
if os.path.exists(rq_job.meta['tmp_file']):
os.remove(rq_job.meta['tmp_file'])
exc_info = str(rq_job.exc_info) or str(rq_job.dependency.exc_info)
if rq_job.dependency:
rq_job.dependency.delete()
rq_job.delete()
return exc_info
def configure_dependent_job(queue, rq_id, rq_func, db_storage, filename, key):
rq_job_id_download_file = rq_id + f'?action=download_{filename}'
rq_job_download_file = queue.fetch_job(rq_job_id_download_file)
if not rq_job_download_file:
# note: boto3 resource isn't pickleable, so we can't use storage
rq_job_download_file = queue.enqueue_call(
func=rq_func,
args=(db_storage, filename, key),
job_id=rq_job_id_download_file
)
return rq_job_download_file

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

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

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

@ -92,6 +92,7 @@ context('Backup, restore a project.', { browser: '!firefox' }, () => {
projectBackupArchiveFullName = file;
cy.verifyDownload(projectBackupArchiveFullName);
});
cy.verifyNotification();
});
it('Remove and restore the project from backup.', () => {
@ -156,6 +157,7 @@ context('Backup, restore a project with a 3D task.', { browser: '!firefox' }, ()
projectBackupArchiveFullName = file;
cy.verifyDownload(projectBackupArchiveFullName);
});
cy.verifyNotification();
});
it('Remove and restore the project from backup.', () => {

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

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

@ -85,26 +85,38 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => {
cy.get('.ant-dropdown')
.not('.ant-dropdown-hidden')
.within(() => {
cy.contains('[role="menuitem"]', new RegExp('^Backup Task$')).click().trigger('mouseout');
cy.contains('[role="menuitem"]', new RegExp('^Backup Task$')).click();
});
cy.get('.cvat-modal-export-task').find('.cvat-modal-export-filename-input').type(archiveName);
cy.get('.cvat-modal-export-task').contains('button', 'OK').click();
cy.get('.cvat-notification-notice-export-backup-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-export-backup-start');
cy.getDownloadFileName().then((file) => {
taskBackupArchiveFullName = file;
cy.verifyDownload(taskBackupArchiveFullName);
});
cy.verifyNotification();
cy.deleteTask(taskName);
});
it('Import the task. Check id, labels, shape.', () => {
cy.intercept({ method: /PATCH|POST/, url: /\/api\/tasks\/backup.*/ }).as('importTask');
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-import-task').click().find('input[type=file]').attachFile(taskBackupArchiveFullName);
cy.get('.cvat-import-task-button').click();
cy.get('input[type=file]').attachFile(taskBackupArchiveFullName, { subjectType: 'drag-n-drop' });
cy.get(`[title="${taskBackupArchiveFullName}"]`).should('be.visible');
cy.contains('button', 'OK').click();
cy.get('.cvat-notification-notice-import-backup-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-import-backup-start');
cy.wait('@importTask').its('response.statusCode').should('equal', 202);
cy.wait('@importTask').its('response.statusCode').should('equal', 201);
cy.wait('@importTask').its('response.statusCode').should('equal', 204);
cy.wait('@importTask').its('response.statusCode').should('equal', 202);
cy.wait('@importTask', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@importTask').its('response.statusCode').should('equal', 201);
cy.contains('Task has been imported succesfully').should('exist').and('be.visible');
cy.contains('The task has been restored succesfully. Click here to open').should('exist').and('be.visible');
cy.closeNotification('.ant-notification-notice-info');
cy.openTask(taskName);
cy.url().then((link) => {
expect(Number(link.split('/').slice(-1)[0])).to.be.equal(taskId + 1);

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

@ -21,9 +21,23 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format',
});
}
function uploadAnnotation(format, file, confirmModalClassName) {
cy.get('.cvat-modal-import-dataset').should('be.visible');
cy.get('.cvat-modal-import-select').click();
cy.contains('.cvat-modal-import-dataset-option-item', format).click();
cy.get('.cvat-modal-import-select').should('contain.text', format);
cy.get('input[type="file"]').attachFile(file, { subjectType: 'drag-n-drop' });
cy.get(`[title="${file}"]`).should('be.visible');
cy.contains('button', 'OK').click();
confirmUpdate(confirmModalClassName);
cy.get('.cvat-notification-notice-import-annotation-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-import-annotation-start');
}
before(() => {
cy.openTask(taskName);
cy.openJob();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams);
cy.saveJob('PATCH', 200, 'saveJob');
@ -36,11 +50,12 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format',
type: 'annotations',
format: dumpTypePC,
};
cy.exportTask(exportAnnotation);
cy.exportJob(exportAnnotation);
cy.getDownloadFileName().then((file) => {
annotationPCArchiveName = file;
cy.verifyDownload(annotationPCArchiveName);
});
cy.verifyNotification();
});
it('Export with "Point Cloud" format. Renaming the archive', () => {
@ -48,35 +63,29 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format',
as: 'exportAnnotationsRenameArchive',
type: 'annotations',
format: dumpTypePC,
archiveCustomeName: 'task_export_3d_annotation_custome_name_pc_format',
archiveCustomeName: 'job_export_3d_annotation_custome_name_pc_format',
};
cy.exportTask(exportAnnotationRenameArchive);
cy.exportJob(exportAnnotationRenameArchive);
cy.getDownloadFileName().then((file) => {
annotationPCArchiveCustomeName = file;
cy.verifyDownload(annotationPCArchiveCustomeName);
});
cy.verifyNotification();
cy.removeAnnotations();
cy.saveJob('PUT');
cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist');
});
it('Upload "Point Cloud" format annotation to job.', () => {
cy.interactMenu('Upload annotations');
cy.readFile(`cypress/fixtures/${annotationPCArchiveName}`, 'binary')
.then(Cypress.Blob.binaryStringToBlob)
.then((fileContent) => {
cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0])
.should('be.visible')
.within(() => {
cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({
fileContent,
fileName: annotationPCArchiveName,
});
});
});
confirmUpdate('.cvat-modal-content-load-job-annotation');
cy.intercept('GET', '/api/jobs/**/annotations**').as('uploadAnnotationsGet');
cy.interactMenu('Upload annotations');
uploadAnnotation(
dumpTypePC.split(' ')[0],
annotationPCArchiveName,
'.cvat-modal-content-load-job-annotation',
);
cy.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200);
cy.verifyNotification();
cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations();
cy.get('button').contains('Save').click().trigger('mouseout');
@ -89,22 +98,14 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format',
.parents('.cvat-tasks-list-item')
.find('.cvat-menu-icon')
.trigger('mouseover');
cy.contains('Upload annotations').trigger('mouseover');
cy.readFile(`cypress/fixtures/${annotationPCArchiveCustomeName}`, 'binary')
.then(Cypress.Blob.binaryStringToBlob)
.then((fileContent) => {
cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0])
.should('be.visible')
.within(() => {
cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({
fileName: annotationPCArchiveCustomeName,
fileContent,
});
});
});
confirmUpdate('.cvat-modal-content-load-task-annotation');
cy.contains('Upload annotations').click();
uploadAnnotation(
dumpTypePC.split(' ')[0],
annotationPCArchiveName,
'.cvat-modal-content-load-task-annotation',
);
cy.contains('Annotations have been loaded').should('be.visible');
cy.get('[data-icon="close"]').click();
cy.closeNotification('.ant-notification-notice-info');
cy.openTaskJob(taskName);
cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations();

@ -21,9 +21,23 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form
});
}
function uploadAnnotation(format, file, confirmModalClassName) {
cy.get('.cvat-modal-import-dataset').should('be.visible');
cy.get('.cvat-modal-import-select').click();
cy.contains('.cvat-modal-import-dataset-option-item', format).click();
cy.get('.cvat-modal-import-select').should('contain.text', format);
cy.get('input[type="file"]').attachFile(file, { subjectType: 'drag-n-drop' });
cy.get(`[title="${file}"]`).should('be.visible');
cy.contains('button', 'OK').click();
confirmUpdate(confirmModalClassName);
cy.get('.cvat-notification-notice-import-annotation-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-import-annotation-start');
}
before(() => {
cy.openTask(taskName);
cy.openJob();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams);
cy.saveJob('PATCH', 200, 'saveJob');
@ -36,11 +50,12 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form
type: 'annotations',
format: dumpTypeVC,
};
cy.exportTask(exportAnnotation);
cy.exportJob(exportAnnotation);
cy.getDownloadFileName().then((file) => {
annotationVCArchiveName = file;
cy.verifyDownload(annotationVCArchiveName);
});
cy.verifyNotification();
});
it('Export with "Point Cloud" format. Renaming the archive', () => {
@ -48,35 +63,30 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form
as: 'exportAnnotationsRenameArchive',
type: 'annotations',
format: dumpTypeVC,
archiveCustomeName: 'task_export_3d_annotation_custome_name_vc_format',
archiveCustomeName: 'job_export_3d_annotation_custome_name_vc_format',
};
cy.exportTask(exportAnnotationRenameArchive);
cy.exportJob(exportAnnotationRenameArchive);
cy.getDownloadFileName().then((file) => {
annotationVCArchiveNameCustomeName = file;
cy.verifyDownload(annotationVCArchiveNameCustomeName);
});
cy.verifyNotification();
cy.removeAnnotations();
cy.saveJob('PUT');
cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist');
});
it('Upload "Velodyne Points" format annotation to job.', () => {
cy.interactMenu('Upload annotations');
cy.readFile(`cypress/fixtures/${annotationVCArchiveName}`, 'binary')
.then(Cypress.Blob.binaryStringToBlob)
.then((fileContent) => {
cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0])
.should('be.visible')
.within(() => {
cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({
fileContent,
fileName: annotationVCArchiveName,
});
});
});
confirmUpdate('.cvat-modal-content-load-job-annotation');
cy.intercept('GET', '/api/jobs/**/annotations**').as('uploadAnnotationsGet');
cy.interactMenu('Upload annotations');
uploadAnnotation(
dumpTypeVC.split(' ')[0],
annotationVCArchiveName,
'.cvat-modal-content-load-job-annotation',
);
cy.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200);
cy.contains('Annotations have been loaded').should('be.visible');
cy.closeNotification('.ant-notification-notice-info');
cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations();
cy.get('button').contains('Save').click().trigger('mouseout');
@ -88,22 +98,14 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form
.parents('.cvat-tasks-list-item')
.find('.cvat-menu-icon')
.trigger('mouseover');
cy.contains('Upload annotations').trigger('mouseover');
cy.readFile(`cypress/fixtures/${annotationVCArchiveNameCustomeName}`, 'binary')
.then(Cypress.Blob.binaryStringToBlob)
.then((fileContent) => {
cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0])
.should('be.visible')
.within(() => {
cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({
fileName: annotationVCArchiveNameCustomeName,
fileContent,
});
});
});
confirmUpdate('.cvat-modal-content-load-task-annotation');
cy.contains('Upload annotations').click();
uploadAnnotation(
dumpTypeVC.split(' ')[0],
annotationVCArchiveNameCustomeName,
'.cvat-modal-content-load-task-annotation',
);
cy.contains('Annotations have been loaded').should('be.visible');
cy.get('[data-icon="close"]').click();
cy.closeNotification('.ant-notification-notice-info');
cy.openTaskJob(taskName);
cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations();

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

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

@ -864,7 +864,7 @@ Cypress.Commands.add('exportTask', ({
cy.contains('.cvat-modal-export-option-item', format).should('be.visible').click();
cy.get('.cvat-modal-export-task').find('.cvat-modal-export-select').should('contain.text', format);
if (type === 'dataset') {
cy.get('.cvat-modal-export-task').find('[type="checkbox"]').should('not.be.checked').check();
cy.get('.cvat-modal-export-task').find('.cvat-modal-export-save-images').should('not.be.checked').click();
}
if (archiveCustomeName) {
cy.get('.cvat-modal-export-task').find('.cvat-modal-export-filename-input').type(archiveCustomeName);
@ -874,6 +874,24 @@ Cypress.Commands.add('exportTask', ({
cy.closeNotification('.cvat-notification-notice-export-task-start');
});
Cypress.Commands.add('exportJob', ({
type, format, archiveCustomeName,
}) => {
cy.interactMenu('Export job dataset');
cy.get('.cvat-modal-export-job').should('be.visible').find('.cvat-modal-export-select').click();
cy.contains('.cvat-modal-export-option-item', format).should('be.visible').click();
cy.get('.cvat-modal-export-job').find('.cvat-modal-export-select').should('contain.text', format);
if (type === 'dataset') {
cy.get('.cvat-modal-export-job').find('.cvat-modal-export-save-images').should('not.be.checked').click();
}
if (archiveCustomeName) {
cy.get('.cvat-modal-export-job').find('.cvat-modal-export-filename-input').type(archiveCustomeName);
}
cy.contains('button', 'OK').click();
cy.get('.cvat-notification-notice-export-job-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-export-job-start');
});
Cypress.Commands.add('renameTask', (oldName, newName) => {
cy.get('.cvat-task-details-task-name').within(() => {
cy.get('[aria-label="edit"]').click();
@ -928,6 +946,11 @@ Cypress.Commands.add('deleteFrame', (action = 'delete') => {
cy.wait('@patchMeta').its('response.statusCode').should('equal', 200);
});
Cypress.Commands.add('verifyNotification', () => {
cy.get('.ant-notification-notice-info').should('be.visible');
cy.closeNotification('.ant-notification-notice-info');
});
Cypress.Commands.overwrite('visit', (orig, url, options) => {
orig(url, options);
cy.closeModalUnsupportedPlatform();

@ -104,13 +104,14 @@ Cypress.Commands.add('exportProject', ({
cy.contains('.cvat-modal-export-option-item', dumpType).should('be.visible').click();
cy.get('.cvat-modal-export-select').should('contain.text', dumpType);
if (type === 'dataset') {
cy.get('.cvat-modal-export-project').find('[type="checkbox"]').should('not.be.checked').check();
cy.get('.cvat-modal-export-project').find('.cvat-modal-export-save-images').should('not.be.checked').click();
}
if (archiveCustomeName) {
cy.get('.cvat-modal-export-project').find('.cvat-modal-export-filename-input').type(archiveCustomeName);
}
cy.get('.cvat-modal-export-project').contains('button', 'OK').click();
cy.get('.cvat-notification-notice-export-project-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-export-project-start');
});
Cypress.Commands.add('importProject', ({
@ -131,28 +132,42 @@ Cypress.Commands.add('importProject', ({
cy.contains('button', 'OK').click();
cy.get('.cvat-modal-import-dataset-status').should('be.visible');
cy.get('.cvat-notification-notice-import-dataset-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-import-dataset-start');
cy.get('.cvat-modal-import-dataset-status').should('not.exist');
});
Cypress.Commands.add('backupProject', (projectName) => {
Cypress.Commands.add('backupProject', (projectName, backupFileName) => {
cy.projectActions(projectName);
cy.get('.cvat-project-actions-menu').contains('Backup Project').click();
cy.get('.cvat-modal-export-project').should('be.visible');
if (backupFileName) {
cy.get('.cvat-modal-export-project').find('.cvat-modal-export-filename-input').type(backupFileName);
}
cy.get('.cvat-modal-export-project').contains('button', 'OK').click();
cy.get('.cvat-notification-notice-export-backup-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-export-backup-start');
});
Cypress.Commands.add('restoreProject', (archiveWithBackup) => {
cy.intercept({ method: /PATCH|POST/, url: /\/api\/projects\/backup.*/ }).as('restoreProject');
cy.get('.cvat-create-project-dropdown').click();
cy.get('.cvat-import-project').click().find('input[type=file]').attachFile(archiveWithBackup);
cy.get('.cvat-import-project-button').click();
cy.get('input[type=file]').attachFile(archiveWithBackup, { subjectType: 'drag-n-drop' });
cy.get(`[title="${archiveWithBackup}"]`).should('be.visible');
cy.contains('button', 'OK').click();
cy.get('.cvat-notification-notice-import-backup-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-import-backup-start');
cy.wait('@restoreProject').its('response.statusCode').should('equal', 202);
cy.wait('@restoreProject').its('response.statusCode').should('equal', 201);
cy.wait('@restoreProject').its('response.statusCode').should('equal', 204);
cy.wait('@restoreProject').its('response.statusCode').should('equal', 202);
cy.wait('@restoreProject', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@restoreProject').its('response.statusCode').should('equal', 201);
cy.contains('Project has been created succesfully')
cy.contains('The project has been restored succesfully. Click here to open')
.should('exist')
.and('be.visible');
cy.get('[data-icon="close"]').click(); // Close the notification
cy.closeNotification('.ant-notification-notice-info');
});
Cypress.Commands.add('getDownloadFileName', () => {
@ -168,6 +183,7 @@ Cypress.Commands.add('waitForDownload', () => {
cy.getDownloadFileName().then((filename) => {
cy.verifyDownload(filename);
});
cy.verifyNotification();
});
Cypress.Commands.add('deleteProjectViaActions', (projectName) => {

Loading…
Cancel
Save