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