Source & target storage support (#4842)
parent
a50d38f9e9
commit
9f89787f95
@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue