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