parent
6df808dfce
commit
9a53879a8d
@ -0,0 +1,194 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Dispatch, ActionCreator } from 'redux';
|
||||
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
|
||||
import getCore from 'cvat-core-wrapper';
|
||||
import { CloudStoragesQuery, CloudStorage } from 'reducers/interfaces';
|
||||
|
||||
const cvat = getCore();
|
||||
|
||||
export enum CloudStorageActionTypes {
|
||||
UPDATE_CLOUD_STORAGES_GETTING_QUERY = 'UPDATE_CLOUD_STORAGES_GETTING_QUERY',
|
||||
GET_CLOUD_STORAGES = 'GET_CLOUD_STORAGES',
|
||||
GET_CLOUD_STORAGE_SUCCESS = 'GET_CLOUD_STORAGES_SUCCESS',
|
||||
GET_CLOUD_STORAGE_FAILED = 'GET_CLOUD_STORAGES_FAILED',
|
||||
GET_CLOUD_STORAGE_STATUS = 'GET_CLOUD_STORAGE_STATUS',
|
||||
GET_CLOUD_STORAGE_STATUS_SUCCESS = 'GET_CLOUD_STORAGE_STATUS_SUCCESS',
|
||||
GET_CLOUD_STORAGE_STATUS_FAILED = 'GET_CLOUD_STORAGE_STATUS_FAILED',
|
||||
GET_CLOUD_STORAGE_PREVIEW_FAILED = 'GET_CLOUD_STORAGE_PREVIEW_FAILED',
|
||||
CREATE_CLOUD_STORAGE = 'CREATE_CLOUD_STORAGE',
|
||||
CREATE_CLOUD_STORAGE_SUCCESS = 'CREATE_CLOUD_STORAGE_SUCCESS',
|
||||
CREATE_CLOUD_STORAGE_FAILED = 'CREATE_CLOUD_STORAGE_FAILED',
|
||||
DELETE_CLOUD_STORAGE = 'DELETE_CLOUD_STORAGE',
|
||||
DELETE_CLOUD_STORAGE_SUCCESS = 'DELETE_CLOUD_STORAGE_SUCCESS',
|
||||
DELETE_CLOUD_STORAGE_FAILED = 'DELETE_CLOUD_STORAGE_FAILED',
|
||||
UPDATE_CLOUD_STORAGE = 'UPDATE_CLOUD_STORAGE',
|
||||
UPDATE_CLOUD_STORAGE_SUCCESS = 'UPDATE_CLOUD_STORAGE_SUCCESS',
|
||||
UPDATE_CLOUD_STORAGE_FAILED = 'UPDATE_CLOUD_STORAGE_FAILED',
|
||||
LOAD_CLOUD_STORAGE_CONTENT = 'LOAD_CLOUD_STORAGE_CONTENT',
|
||||
LOAD_CLOUD_STORAGE_CONTENT_FAILED = 'LOAD_CLOUD_STORAGE_CONTENT_FAILED',
|
||||
LOAD_CLOUD_STORAGE_CONTENT_SUCCESS = 'LOAD_CLOUD_STORAGE_CONTENT_SUCCESS',
|
||||
}
|
||||
|
||||
const cloudStoragesActions = {
|
||||
updateCloudStoragesGettingQuery: (query: Partial<CloudStoragesQuery>) =>
|
||||
createAction(CloudStorageActionTypes.UPDATE_CLOUD_STORAGES_GETTING_QUERY, { query }),
|
||||
getCloudStorages: () => createAction(CloudStorageActionTypes.GET_CLOUD_STORAGES),
|
||||
getCloudStoragesSuccess: (
|
||||
array: any[],
|
||||
previews: string[],
|
||||
statuses: string[],
|
||||
count: number,
|
||||
query: Partial<CloudStoragesQuery>,
|
||||
) =>
|
||||
createAction(CloudStorageActionTypes.GET_CLOUD_STORAGE_SUCCESS, {
|
||||
array,
|
||||
previews,
|
||||
statuses,
|
||||
count,
|
||||
query,
|
||||
}),
|
||||
getCloudStoragesFailed: (error: any, query: Partial<CloudStoragesQuery>) =>
|
||||
createAction(CloudStorageActionTypes.GET_CLOUD_STORAGE_FAILED, { error, query }),
|
||||
deleteCloudStorage: (cloudStorageID: number) =>
|
||||
createAction(CloudStorageActionTypes.DELETE_CLOUD_STORAGE, { cloudStorageID }),
|
||||
deleteCloudStorageSuccess: (cloudStorageID: number) =>
|
||||
createAction(CloudStorageActionTypes.DELETE_CLOUD_STORAGE_SUCCESS, { cloudStorageID }),
|
||||
deleteCloudStorageFailed: (error: any, cloudStorageID: number) =>
|
||||
createAction(CloudStorageActionTypes.DELETE_CLOUD_STORAGE_FAILED, { error, cloudStorageID }),
|
||||
createCloudStorage: () => createAction(CloudStorageActionTypes.CREATE_CLOUD_STORAGE),
|
||||
createCloudStorageSuccess: (cloudStorageID: number) =>
|
||||
createAction(CloudStorageActionTypes.CREATE_CLOUD_STORAGE_SUCCESS, { cloudStorageID }),
|
||||
createCloudStorageFailed: (error: any) =>
|
||||
createAction(CloudStorageActionTypes.CREATE_CLOUD_STORAGE_FAILED, { error }),
|
||||
updateCloudStorage: () => createAction(CloudStorageActionTypes.UPDATE_CLOUD_STORAGE, {}),
|
||||
updateCloudStorageSuccess: (cloudStorage: CloudStorage) =>
|
||||
createAction(CloudStorageActionTypes.UPDATE_CLOUD_STORAGE_SUCCESS, { cloudStorage }),
|
||||
updateCloudStorageFailed: (cloudStorage: CloudStorage, error: any) =>
|
||||
createAction(CloudStorageActionTypes.UPDATE_CLOUD_STORAGE_FAILED, { cloudStorage, error }),
|
||||
loadCloudStorageContent: () => createAction(CloudStorageActionTypes.LOAD_CLOUD_STORAGE_CONTENT),
|
||||
loadCloudStorageContentSuccess: (cloudStorageID: number, content: any) =>
|
||||
createAction(CloudStorageActionTypes.LOAD_CLOUD_STORAGE_CONTENT_SUCCESS, { cloudStorageID, content }),
|
||||
loadCloudStorageContentFailed: (cloudStorageID: number, error: any) =>
|
||||
createAction(CloudStorageActionTypes.LOAD_CLOUD_STORAGE_CONTENT_FAILED, { cloudStorageID, error }),
|
||||
getCloudStorageStatus: () => createAction(CloudStorageActionTypes.GET_CLOUD_STORAGE_STATUS),
|
||||
getCloudStorageStatusSuccess: (cloudStorageID: number, status: string) =>
|
||||
createAction(CloudStorageActionTypes.GET_CLOUD_STORAGE_STATUS_SUCCESS, { cloudStorageID, status }),
|
||||
getCloudStorageStatusFailed: (cloudStorageID: number, error: any) =>
|
||||
createAction(CloudStorageActionTypes.GET_CLOUD_STORAGE_STATUS_FAILED, { cloudStorageID, error }),
|
||||
getCloudStoragePreiewFailed: (cloudStorageID: number, error: any) =>
|
||||
createAction(CloudStorageActionTypes.GET_CLOUD_STORAGE_PREVIEW_FAILED, { cloudStorageID, error }),
|
||||
};
|
||||
|
||||
export type CloudStorageActions = ActionUnion<typeof cloudStoragesActions>;
|
||||
|
||||
export function getCloudStoragesAsync(query: Partial<CloudStoragesQuery>): ThunkAction {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
dispatch(cloudStoragesActions.getCloudStorages());
|
||||
dispatch(cloudStoragesActions.updateCloudStoragesGettingQuery(query));
|
||||
|
||||
const filteredQuery = { ...query };
|
||||
for (const key in filteredQuery) {
|
||||
if (filteredQuery[key] === null) {
|
||||
delete filteredQuery[key];
|
||||
}
|
||||
}
|
||||
|
||||
let result = null;
|
||||
try {
|
||||
result = await cvat.cloudStorages.get(filteredQuery);
|
||||
} catch (error) {
|
||||
dispatch(cloudStoragesActions.getCloudStoragesFailed(error, query));
|
||||
return;
|
||||
}
|
||||
|
||||
const array = Array.from(result);
|
||||
const promises = array.map((cloudStorage: CloudStorage): string =>
|
||||
(cloudStorage as any).getPreview().catch((error: any) => {
|
||||
dispatch(cloudStoragesActions.getCloudStoragePreiewFailed(cloudStorage.id, error));
|
||||
return '';
|
||||
}));
|
||||
|
||||
const statusPromises = array.map((cloudStorage: CloudStorage): string =>
|
||||
(cloudStorage as any).getStatus().catch((error: any) => {
|
||||
dispatch(cloudStoragesActions.getCloudStorageStatusFailed(cloudStorage.id, error));
|
||||
return '';
|
||||
}));
|
||||
|
||||
dispatch(cloudStoragesActions.getCloudStoragesSuccess(
|
||||
array,
|
||||
await Promise.all(promises),
|
||||
await Promise.all(statusPromises),
|
||||
result.count,
|
||||
query,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteCloudStorageAsync(cloudStorageInstance: any): ThunkAction {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
try {
|
||||
dispatch(cloudStoragesActions.deleteCloudStorage(cloudStorageInstance.id));
|
||||
await cloudStorageInstance.delete();
|
||||
} catch (error) {
|
||||
dispatch(cloudStoragesActions.deleteCloudStorageFailed(error, cloudStorageInstance.id));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(cloudStoragesActions.deleteCloudStorageSuccess(cloudStorageInstance.id));
|
||||
};
|
||||
}
|
||||
|
||||
export function createCloudStorageAsync(data: any): ThunkAction {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
const cloudStorageInstance = new cvat.classes.CloudStorage(data);
|
||||
|
||||
dispatch(cloudStoragesActions.createCloudStorage());
|
||||
try {
|
||||
const savedCloudStorage = await cloudStorageInstance.save();
|
||||
dispatch(cloudStoragesActions.createCloudStorageSuccess(savedCloudStorage.id));
|
||||
} catch (error) {
|
||||
dispatch(cloudStoragesActions.createCloudStorageFailed(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCloudStorageAsync(data: any): ThunkAction {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
const cloudStorageInstance = new cvat.classes.CloudStorage(data);
|
||||
|
||||
dispatch(cloudStoragesActions.updateCloudStorage());
|
||||
try {
|
||||
const savedCloudStorage = await cloudStorageInstance.save();
|
||||
dispatch(cloudStoragesActions.updateCloudStorageSuccess(savedCloudStorage));
|
||||
} catch (error) {
|
||||
dispatch(cloudStoragesActions.updateCloudStorageFailed(data, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadCloudStorageContentAsync(cloudStorage: CloudStorage): ThunkAction {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
dispatch(cloudStoragesActions.loadCloudStorageContent());
|
||||
try {
|
||||
const result = await cloudStorage.getContent();
|
||||
dispatch(cloudStoragesActions.loadCloudStorageContentSuccess(cloudStorage.id, result));
|
||||
} catch (error) {
|
||||
dispatch(cloudStoragesActions.loadCloudStorageContentFailed(cloudStorage.id, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// export function getCloudStorageStatusAsync(cloudStorage: CloudStorage): ThunkAction {
|
||||
// return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
// dispatch(cloudStoragesActions.getCloudStorageStatus());
|
||||
// try {
|
||||
// const result = await cloudStorage.getStatus();
|
||||
// dispatch(cloudStoragesActions.getCloudStorageStatusSuccess(cloudStorage.id, result));
|
||||
// } catch (error) {
|
||||
// dispatch(cloudStoragesActions.getCloudStorageStatusFailed(cloudStorage.id, error));
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@ -0,0 +1,21 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M1.289 2.771L0 3.30333V12.6463L1.289 13.1755L1.29675 13.1678V2.77833L1.289 2.771Z" fill="#8C3123"/>
|
||||
<path d="M8.18756 11.8195L1.28906 13.1755V2.771L8.18756 4.0975V11.8195Z" fill="#E05243"/>
|
||||
<path d="M5.07349 9.69626L7.99961 10.0039L8.01798 9.96888L8.03442 6.00655L7.99961 5.97559L5.07349 6.27876V9.69626Z" fill="#8C3123"/>
|
||||
<path d="M7.99976 11.8347L14.7104 13.1784L14.721 13.1645L14.7208 2.78029L14.7102 2.771L7.99976 4.11273V11.8347Z" fill="#8C3123"/>
|
||||
<path d="M10.9267 9.69626L7.99976 10.0039V5.97559L10.9267 6.27876V9.69626Z" fill="#E05243"/>
|
||||
<path d="M10.9265 4.62622L7.99961 5.06674L5.07349 4.62622L7.99592 3.99365L10.9265 4.62622Z" fill="#5E1F18"/>
|
||||
<path d="M10.9265 11.3448L7.99961 10.9014L5.07349 11.3448L7.99605 12.0185L10.9265 11.3448Z" fill="#F2B0A9"/>
|
||||
<path d="M5.07349 4.62612L7.99961 4.02813L8.0233 4.02209V0.0161548L7.99961 0L5.07349 1.20841V4.62612Z" fill="#8C3123"/>
|
||||
<path d="M10.9267 4.62612L7.99976 4.02813V0L10.9267 1.20841V4.62612Z" fill="#E05243"/>
|
||||
<path d="M7.99968 15.9704L5.07324 14.7624V11.3447L7.99968 11.9425L8.04274 11.9829L8.03106 15.9006L7.99968 15.9704Z" fill="#8C3123"/>
|
||||
<path d="M7.99976 15.9704L10.9264 14.7624V11.3447L7.99976 11.9425V15.9704Z" fill="#E05243"/>
|
||||
<path d="M14.7104 2.771L16 3.30333V12.6463L14.7104 13.1784V2.771Z" fill="#E05243"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.98 25.9939C18.55 25.2679 21.498 24.6669 21.532 24.6589L21.594 24.6439L18.224 20.0289C16.37 17.4909 14.854 15.4039 14.854 15.3919C14.854 15.3799 18.334 4.3359 18.354 4.2969C18.361 4.2839 20.729 8.9909 24.095 15.7079L29.869 27.2289L29.913 27.3169H8.49097L14.98 25.9939Z" fill="#0089D6"/>
|
||||
<path d="M2.125 24.586C2.125 24.58 3.713 21.406 5.654 17.533L9.183 10.492L13.3 6.52002C15.562 4.33502 17.419 2.54402 17.426 2.54102C17.4112 2.60727 17.3891 2.67167 17.36 2.73302L12.89 13.759L8.5 24.589H5.311C4.24902 24.5999 3.18696 24.5989 2.125 24.586V24.586Z" fill="#0089D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 683 B |
@ -0,0 +1,145 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import { CloudSyncOutlined, MoreOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import Card from 'antd/lib/card';
|
||||
import Meta from 'antd/lib/card/Meta';
|
||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Button from 'antd/lib/button';
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import moment from 'moment';
|
||||
|
||||
import { CloudStorage, CombinedState } from 'reducers/interfaces';
|
||||
import { deleteCloudStorageAsync } from 'actions/cloud-storage-actions';
|
||||
import CVATTooltip from 'components/common/cvat-tooltip';
|
||||
import Status from './cloud-storage-status';
|
||||
|
||||
interface Props {
|
||||
cloudStorageInstance: CloudStorage;
|
||||
}
|
||||
|
||||
export default function CloudStorageItemComponent(props: Props): JSX.Element {
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// cloudStorageInstance: {storage, preview, status}
|
||||
const { cloudStorageInstance } = props;
|
||||
const {
|
||||
id,
|
||||
displayName,
|
||||
providerType,
|
||||
owner,
|
||||
createdDate,
|
||||
updatedDate,
|
||||
description,
|
||||
} = cloudStorageInstance.storage;
|
||||
const { preview, status } = cloudStorageInstance;
|
||||
const deletes = useSelector((state: CombinedState) => state.cloudStorages.activities.deletes);
|
||||
const deleted = cloudStorageInstance.storage.id in deletes ? deletes[cloudStorageInstance.storage.id] : false;
|
||||
|
||||
const style: React.CSSProperties = {};
|
||||
|
||||
if (deleted) {
|
||||
style.pointerEvents = 'none';
|
||||
style.opacity = 0.5;
|
||||
}
|
||||
|
||||
const onUpdate = useCallback(() => {
|
||||
history.push(`/cloudstorages/update/${id}`);
|
||||
}, []);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
Modal.confirm({
|
||||
title: 'Please, confirm your action',
|
||||
content: `You are going to remove the cloudstorage "${displayName}". Continue?`,
|
||||
className: 'cvat-delete-cloud-storage-modal',
|
||||
onOk: () => {
|
||||
dispatch(deleteCloudStorageAsync(cloudStorageInstance.storage));
|
||||
},
|
||||
okButtonProps: {
|
||||
type: 'primary',
|
||||
danger: true,
|
||||
},
|
||||
okText: 'Delete',
|
||||
});
|
||||
}, [cloudStorageInstance.storage.id]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
cover={(
|
||||
<>
|
||||
{preview ? (
|
||||
<img
|
||||
className='cvat-cloud-storage-item-preview'
|
||||
src={preview}
|
||||
alt='Preview image'
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
<div className='cvat-cloud-storage-item-empty-preview' aria-hidden>
|
||||
<CloudSyncOutlined />
|
||||
</div>
|
||||
)}
|
||||
{description ? (
|
||||
<CVATTooltip overlay={description}>
|
||||
<QuestionCircleOutlined className='cvat-cloud-storage-description-icon' />
|
||||
</CVATTooltip>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
size='small'
|
||||
style={style}
|
||||
className='cvat-cloud-storage-item'
|
||||
>
|
||||
<Meta
|
||||
title={(
|
||||
<Paragraph>
|
||||
<Text strong>{`#${id}: `}</Text>
|
||||
<Text>{displayName}</Text>
|
||||
</Paragraph>
|
||||
)}
|
||||
description={(
|
||||
<>
|
||||
<Paragraph>
|
||||
<Text type='secondary'>Provider: </Text>
|
||||
<Text>{providerType}</Text>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Text type='secondary'>Created </Text>
|
||||
{owner ? <Text type='secondary'>{`by ${owner.username}`}</Text> : null}
|
||||
<Text type='secondary'> on </Text>
|
||||
<Text type='secondary'>{moment(createdDate).format('MMMM Do YYYY')}</Text>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Text type='secondary'>Last updated </Text>
|
||||
<Text type='secondary'>{moment(updatedDate).fromNow()}</Text>
|
||||
</Paragraph>
|
||||
<Status status={status} />
|
||||
<Dropdown
|
||||
overlay={(
|
||||
<Menu className='cvat-project-actions-menu'>
|
||||
<Menu.Item onClick={onUpdate}>Update</Menu.Item>
|
||||
<Menu.Item onClick={onDelete}>Delete</Menu.Item>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className='cvat-cloud-storage-item-menu-button'
|
||||
type='link'
|
||||
size='large'
|
||||
icon={<MoreOutlined />}
|
||||
/>
|
||||
</Dropdown>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { StorageStatuses } from '../../utils/enums';
|
||||
|
||||
interface Props {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export default function Status(props: Props): JSX.Element {
|
||||
const { status } = props;
|
||||
// TODO: make dynamic loading of statuses separately in the future
|
||||
|
||||
return (
|
||||
<Paragraph>
|
||||
<Text type='secondary'>Status: </Text>
|
||||
{status ? (
|
||||
<Text type={status === StorageStatuses.AVAILABLE ? 'success' : 'danger'}>{status}</Text>
|
||||
) : (
|
||||
<Text type='warning'>Loading ...</Text>
|
||||
)}
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import Pagination from 'antd/lib/pagination';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
|
||||
import { CloudStorage } from 'reducers/interfaces';
|
||||
import CloudStorageItemComponent from './cloud-storage-item';
|
||||
|
||||
interface Props {
|
||||
storages: CloudStorage[];
|
||||
previews: string[];
|
||||
statuses: string[];
|
||||
totalCount: number;
|
||||
page: number;
|
||||
onChangePage(page: number): void;
|
||||
}
|
||||
|
||||
export default function StoragesList(props: Props): JSX.Element {
|
||||
const {
|
||||
storages, previews, statuses, totalCount, page, onChangePage,
|
||||
} = props;
|
||||
|
||||
const groupedStorages = storages.reduce(
|
||||
(acc: CloudStorage[][], storage: CloudStorage, index: number): CloudStorage[][] => {
|
||||
if (index && index % 4) {
|
||||
acc[acc.length - 1].push({
|
||||
storage,
|
||||
preview: previews[index],
|
||||
status: statuses[index],
|
||||
});
|
||||
} else {
|
||||
acc.push([{
|
||||
storage,
|
||||
preview: previews[index],
|
||||
status: statuses[index],
|
||||
}]);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col span={24} className='cvat-cloud-storages-list'>
|
||||
{groupedStorages.map(
|
||||
(instances: CloudStorage[]): JSX.Element => (
|
||||
<Row key={instances[0].storage.id} gutter={[8, 8]}>
|
||||
{instances.map((instance: CloudStorage) => (
|
||||
<Col span={6} key={instance.storage.id}>
|
||||
<CloudStorageItemComponent cloudStorageInstance={instance} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
),
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Pagination
|
||||
className='cvat-cloud-storages-pagination'
|
||||
onChange={onChangePage}
|
||||
showSizeChanger={false}
|
||||
total={totalCount}
|
||||
pageSize={12}
|
||||
current={page}
|
||||
showQuickJumper
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Spin from 'antd/lib/spin';
|
||||
|
||||
import { CloudStorage, CloudStoragesQuery, CombinedState } from 'reducers/interfaces';
|
||||
import { getCloudStoragesAsync } from 'actions/cloud-storage-actions';
|
||||
import CloudStoragesListComponent from './cloud-storages-list';
|
||||
import EmptyCloudStorageListComponent from './empty-cloud-storages-list';
|
||||
import TopBarComponent from './top-bar';
|
||||
|
||||
export default function StoragesPageComponent(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const { search } = history.location;
|
||||
const totalCount = useSelector((state: CombinedState) => state.cloudStorages.count);
|
||||
const isFetching = useSelector((state: CombinedState) => state.cloudStorages.fetching);
|
||||
const current = useSelector((state: CombinedState) => state.cloudStorages.current)
|
||||
.map((cloudStrage: CloudStorage) => cloudStrage.instance);
|
||||
const previews = useSelector((state: CombinedState) => state.cloudStorages.current)
|
||||
.map((cloudStrage: CloudStorage) => cloudStrage.preview as string);
|
||||
const statuses = useSelector((state: CombinedState) => state.cloudStorages.current)
|
||||
.map((cloudStrage: CloudStorage) => cloudStrage.status as string);
|
||||
const query = useSelector((state: CombinedState) => state.cloudStorages.gettingQuery);
|
||||
const onSearch = useCallback(
|
||||
(_query: CloudStoragesQuery) => {
|
||||
if (!isFetching) dispatch(getCloudStoragesAsync(_query));
|
||||
},
|
||||
[isFetching],
|
||||
);
|
||||
|
||||
const onChangePage = useCallback(
|
||||
(page: number) => {
|
||||
if (!isFetching && page !== query.page) dispatch(getCloudStoragesAsync({ ...query, page }));
|
||||
},
|
||||
[query],
|
||||
);
|
||||
|
||||
const dimensions = {
|
||||
md: 22,
|
||||
lg: 18,
|
||||
xl: 16,
|
||||
xxl: 16,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value !== null && typeof value !== 'undefined') {
|
||||
searchParams.append(key, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
history.push({
|
||||
pathname: '/cloudstorages',
|
||||
search: `?${searchParams.toString()}`,
|
||||
});
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = { ...query };
|
||||
for (const [key, value] of new URLSearchParams(search)) {
|
||||
if (key in searchParams) {
|
||||
searchParams[key] = ['page', 'id'].includes(key) ? +value : value;
|
||||
}
|
||||
}
|
||||
onSearch(searchParams);
|
||||
}, []);
|
||||
|
||||
const searchWasUsed = Object.entries(query).some(([key, value]) => {
|
||||
if (key === 'page') {
|
||||
return value && Number.isInteger(value) && value > 1;
|
||||
}
|
||||
|
||||
return !!value;
|
||||
});
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<Row className='cvat-cloud-storages-page' justify='center' align='middle'>
|
||||
<Spin size='large' />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className='cvat-cloud-storages-page' justify='center' align='top'>
|
||||
<Col {...dimensions}>
|
||||
<TopBarComponent query={query} onSearch={onSearch} />
|
||||
{current.length ? (
|
||||
<CloudStoragesListComponent
|
||||
totalCount={totalCount}
|
||||
page={query.page}
|
||||
storages={current}
|
||||
previews={previews}
|
||||
statuses={statuses}
|
||||
onChangePage={onChangePage}
|
||||
/>
|
||||
) : (
|
||||
<EmptyCloudStorageListComponent notFound={searchWasUsed} />
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Empty from 'antd/lib/empty';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { CloudTwoTone } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface Props {
|
||||
notFound: boolean;
|
||||
}
|
||||
|
||||
export default function EmptyStoragesListComponent(props: Props): JSX.Element {
|
||||
const { notFound } = props;
|
||||
|
||||
const description = notFound ? (
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text strong>No results matched your search found...</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text strong>No cloud storages attached yet ...</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text type='secondary'>To get started with your cloud storage</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Link to='/cloudstorages/create'>attach a new one</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='cvat-empty-cloud-storages-list'>
|
||||
<Empty description={description} image={<CloudTwoTone className='cvat-empty-cloud-storages-list-icon' />} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-cloud-storages-page {
|
||||
padding: $grid-unit-size * 2;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.ant-spin {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-empty-cloud-storages-list-icon {
|
||||
font-size: $grid-unit-size * 14;
|
||||
}
|
||||
|
||||
.cvat-cloud-storages-pagination {
|
||||
margin-top: $grid-unit-size * 2;
|
||||
}
|
||||
|
||||
.cvat-cloud-storages-list-top-bar {
|
||||
> div:first-child {
|
||||
.cvat-title {
|
||||
margin-right: $grid-unit-size;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-cloud-storages-list,
|
||||
.cvat-empty-cloud-storages-list {
|
||||
margin-top: $grid-unit-size * 2;
|
||||
}
|
||||
|
||||
.cvat-cloud-storage-item {
|
||||
div.ant-typography {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cvat-cloud-storage-item-empty-preview {
|
||||
font-size: $grid-unit-size * 15;
|
||||
text-align: center;
|
||||
height: $grid-unit-size * 24;
|
||||
}
|
||||
|
||||
img {
|
||||
height: $grid-unit-size * 24;
|
||||
width: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.cvat-cloud-storage-item-menu-button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.cvat-cloud-storage-description-icon {
|
||||
position: absolute;
|
||||
top: $grid-unit-size;
|
||||
width: auto;
|
||||
right: $grid-unit-size;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Button from 'antd/lib/button';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
import SearchField from 'components/search-field/search-field';
|
||||
import { CloudStoragesQuery } from 'reducers/interfaces';
|
||||
|
||||
interface Props {
|
||||
onSearch(query: CloudStoragesQuery): void;
|
||||
query: CloudStoragesQuery;
|
||||
}
|
||||
|
||||
export default function StoragesTopBar(props: Props): JSX.Element {
|
||||
const { onSearch, query } = props;
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Row justify='space-between' align='middle' className='cvat-cloud-storages-list-top-bar'>
|
||||
<Col md={11} lg={9} xl={9} xxl={9}>
|
||||
<Text className='cvat-title'>Cloud Storages</Text>
|
||||
<SearchField instance='cloudstorage' onSearch={onSearch} query={query} />
|
||||
</Col>
|
||||
<Col md={{ span: 11 }} lg={{ span: 9 }} xl={{ span: 9 }} xxl={{ span: 9 }}>
|
||||
<Button
|
||||
size='large'
|
||||
className='cvat-attach-cloud-storage-button'
|
||||
type='primary'
|
||||
onClick={(): void => history.push('/cloudstorages/create')}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
Attach a new storage
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,515 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, {
|
||||
useState, useEffect, useRef,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Button from 'antd/lib/button';
|
||||
import Form from 'antd/lib/form';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import notification from 'antd/lib/notification';
|
||||
|
||||
import { CombinedState, CloudStorage } from 'reducers/interfaces';
|
||||
import { createCloudStorageAsync, updateCloudStorageAsync } from 'actions/cloud-storage-actions';
|
||||
import { ProviderType, CredentialsType } from 'utils/enums';
|
||||
import { AzureProvider, S3Provider } from '../../icons';
|
||||
import S3Region from './s3-region';
|
||||
import ManifestsManager from './manifests-manager';
|
||||
|
||||
export interface Props {
|
||||
cloudStorage?: CloudStorage;
|
||||
}
|
||||
|
||||
type CredentialsFormNames = 'key' | 'secret_key' | 'account_name' | 'session_token';
|
||||
type CredentialsCamelCaseNames = 'key' | 'secretKey' | 'accountName' | 'sessionToken';
|
||||
|
||||
interface CloudStorageForm {
|
||||
credentials_type: CredentialsType;
|
||||
display_name: string;
|
||||
provider_type: ProviderType;
|
||||
resource: string;
|
||||
account_name?: string;
|
||||
session_token?: string;
|
||||
key?: string;
|
||||
secret_key?: string;
|
||||
SAS_token?: string;
|
||||
description?: string;
|
||||
region?: string;
|
||||
manifests: string[];
|
||||
}
|
||||
|
||||
export default function CreateCloudStorageForm(props: Props): JSX.Element {
|
||||
const { cloudStorage } = props;
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const [form] = Form.useForm();
|
||||
const shouldShowCreationNotification = useRef(false);
|
||||
const shouldShowUpdationNotification = useRef(false);
|
||||
const [providerType, setProviderType] = useState<ProviderType | null>(null);
|
||||
const [credentialsType, setCredentialsType] = useState<CredentialsType | null>(null);
|
||||
const [selectedRegion, setSelectedRegion] = useState<string | undefined>(undefined);
|
||||
const newCloudStorageId = useSelector((state: CombinedState) => state.cloudStorages.activities.creates.id);
|
||||
const attaching = useSelector((state: CombinedState) => state.cloudStorages.activities.creates.attaching);
|
||||
const updating = useSelector((state: CombinedState) => state.cloudStorages.activities.updates.updating);
|
||||
const updatedCloudStorageId = useSelector(
|
||||
(state: CombinedState) => state.cloudStorages.activities.updates.cloudStorageID,
|
||||
);
|
||||
const loading = cloudStorage ? updating : attaching;
|
||||
const fakeCredentialsData = {
|
||||
accountName: 'X'.repeat(24),
|
||||
sessionToken: 'X'.repeat(300),
|
||||
key: 'X'.repeat(20),
|
||||
secretKey: 'X'.repeat(40),
|
||||
};
|
||||
|
||||
const [keyVisibility, setKeyVisibility] = useState(false);
|
||||
const [secretKeyVisibility, setSecretKeyVisibility] = useState(false);
|
||||
const [sessionTokenVisibility, setSessionTokenVisibility] = useState(false);
|
||||
const [accountNameVisibility, setAccountNameVisibility] = useState(false);
|
||||
|
||||
const [manifestNames, setManifestNames] = useState<string[]>([]);
|
||||
|
||||
function initializeFields(): void {
|
||||
setManifestNames(cloudStorage.manifests);
|
||||
const fieldsValue: CloudStorageForm = {
|
||||
credentials_type: cloudStorage.credentialsType,
|
||||
display_name: cloudStorage.displayName,
|
||||
description: cloudStorage.description,
|
||||
provider_type: cloudStorage.providerType,
|
||||
resource: cloudStorage.resourceName,
|
||||
manifests: manifestNames,
|
||||
};
|
||||
|
||||
setProviderType(cloudStorage.providerType);
|
||||
setCredentialsType(cloudStorage.credentialsType);
|
||||
|
||||
if (cloudStorage.credentialsType === CredentialsType.ACCOUNT_NAME_TOKEN_PAIR) {
|
||||
fieldsValue.account_name = fakeCredentialsData.accountName;
|
||||
fieldsValue.SAS_token = fakeCredentialsData.sessionToken;
|
||||
} else if (cloudStorage.credentialsType === CredentialsType.KEY_SECRET_KEY_PAIR) {
|
||||
fieldsValue.key = fakeCredentialsData.key;
|
||||
fieldsValue.secret_key = fakeCredentialsData.secretKey;
|
||||
}
|
||||
|
||||
if (cloudStorage.providerType === ProviderType.AWS_S3_BUCKET && cloudStorage.specificAttributes) {
|
||||
const region = new URLSearchParams(cloudStorage.specificAttributes).get('region');
|
||||
if (region) {
|
||||
setSelectedRegion(region);
|
||||
}
|
||||
}
|
||||
|
||||
form.setFieldsValue(fieldsValue);
|
||||
}
|
||||
|
||||
function onReset(): void {
|
||||
if (cloudStorage) {
|
||||
initializeFields();
|
||||
} else {
|
||||
setManifestNames([]);
|
||||
setSelectedRegion(undefined);
|
||||
form.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = (): void => {
|
||||
if (history.length) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push('/cloudstorages');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onReset();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
Number.isInteger(newCloudStorageId) &&
|
||||
shouldShowCreationNotification &&
|
||||
shouldShowCreationNotification.current
|
||||
) {
|
||||
// Clear form
|
||||
onReset();
|
||||
|
||||
notification.info({
|
||||
message: 'The cloud storage has been attached',
|
||||
className: 'cvat-notification-create-cloud-storage-success',
|
||||
});
|
||||
}
|
||||
if (shouldShowCreationNotification !== undefined) {
|
||||
shouldShowCreationNotification.current = true;
|
||||
}
|
||||
}, [newCloudStorageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedCloudStorageId && shouldShowUpdationNotification && shouldShowUpdationNotification.current) {
|
||||
notification.info({
|
||||
message: 'The cloud storage has been updated',
|
||||
className: 'cvat-notification-update-cloud-storage-success',
|
||||
});
|
||||
}
|
||||
if (shouldShowUpdationNotification !== undefined) {
|
||||
shouldShowUpdationNotification.current = true;
|
||||
}
|
||||
}, [updatedCloudStorageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cloudStorage && cloudStorage.credentialsType !== CredentialsType.ANONYMOUS_ACCESS) {
|
||||
notification.info({
|
||||
message: `For security reasons, your credentials are hidden and represented by fake values
|
||||
that will not be taken into account when updating the cloud storage.
|
||||
If you want to replace the original credentials, simply enter new ones.`,
|
||||
className: 'cvat-notification-update-info-cloud-storage',
|
||||
duration: 15,
|
||||
});
|
||||
}
|
||||
}, [cloudStorage]);
|
||||
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
let cloudStorageData: Record<string, any> = {};
|
||||
const formValues = await form.validateFields();
|
||||
cloudStorageData = { ...formValues };
|
||||
if (formValues.region !== undefined) {
|
||||
delete cloudStorageData.region;
|
||||
cloudStorageData.specific_attributes = `region=${selectedRegion}`;
|
||||
}
|
||||
|
||||
if (cloudStorageData.credentials_type === CredentialsType.ACCOUNT_NAME_TOKEN_PAIR) {
|
||||
delete cloudStorageData.SAS_token;
|
||||
cloudStorageData.session_token = formValues.SAS_token;
|
||||
}
|
||||
|
||||
if (cloudStorageData.manifests && cloudStorageData.manifests.length) {
|
||||
delete cloudStorageData.manifests;
|
||||
cloudStorageData.manifests = form
|
||||
.getFieldValue('manifests')
|
||||
.map((manifest: any): string => manifest.name);
|
||||
}
|
||||
|
||||
if (cloudStorage) {
|
||||
cloudStorageData.id = cloudStorage.id;
|
||||
if (cloudStorageData.account_name === fakeCredentialsData.accountName) {
|
||||
delete cloudStorageData.account_name;
|
||||
}
|
||||
if (cloudStorageData.key === fakeCredentialsData.key) {
|
||||
delete cloudStorageData.key;
|
||||
}
|
||||
if (cloudStorageData.secret_key === fakeCredentialsData.secretKey) {
|
||||
delete cloudStorageData.secret_key;
|
||||
}
|
||||
if (cloudStorageData.session_token === fakeCredentialsData.sessionToken) {
|
||||
delete cloudStorageData.session_token;
|
||||
}
|
||||
dispatch(updateCloudStorageAsync(cloudStorageData));
|
||||
} else {
|
||||
dispatch(createCloudStorageAsync(cloudStorageData));
|
||||
}
|
||||
};
|
||||
|
||||
const resetCredentialsValues = (): void => {
|
||||
form.setFieldsValue({
|
||||
key: undefined,
|
||||
secret_key: undefined,
|
||||
session_token: undefined,
|
||||
account_name: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const onFocusCredentialsItem = (credential: CredentialsCamelCaseNames, key: CredentialsFormNames): void => {
|
||||
// reset fake credential when updating a cloud storage and cursor is in this field
|
||||
if (cloudStorage && form.getFieldValue(key) === fakeCredentialsData[credential]) {
|
||||
form.setFieldsValue({
|
||||
[key]: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onBlurCredentialsItem = (
|
||||
credential: CredentialsCamelCaseNames,
|
||||
key: CredentialsFormNames,
|
||||
setVisibility: any,
|
||||
): void => {
|
||||
// set fake credential when updating a cloud storage and cursor disappears from the field and value not changed
|
||||
if (cloudStorage && !form.getFieldValue(key)) {
|
||||
form.setFieldsValue({
|
||||
[key]: fakeCredentialsData[credential],
|
||||
});
|
||||
setVisibility(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeCredentialsType = (value: CredentialsType): void => {
|
||||
setCredentialsType(value);
|
||||
resetCredentialsValues();
|
||||
};
|
||||
|
||||
const onSelectRegion = (key: string): void => {
|
||||
setSelectedRegion(key);
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
className: 'cvat-cloud-storage-form-item',
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { offset: 1 },
|
||||
};
|
||||
|
||||
const credentialsBlok = (): JSX.Element => {
|
||||
const internalCommonProps = {
|
||||
...commonProps,
|
||||
labelCol: { span: 8, offset: 2 },
|
||||
wrapperCol: { offset: 1 },
|
||||
};
|
||||
|
||||
if (providerType === ProviderType.AWS_S3_BUCKET && credentialsType === CredentialsType.KEY_SECRET_KEY_PAIR) {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label='ACCESS KEY ID'
|
||||
name='key'
|
||||
rules={[{ required: true, message: 'Please, specify your access_key_id' }]}
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Input.Password
|
||||
maxLength={20}
|
||||
visibilityToggle={keyVisibility}
|
||||
onChange={() => setKeyVisibility(true)}
|
||||
onFocus={() => onFocusCredentialsItem('key', 'key')}
|
||||
onBlur={() => onBlurCredentialsItem('key', 'key', setKeyVisibility)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label='SECRET ACCESS KEY ID'
|
||||
name='secret_key'
|
||||
rules={[{ required: true, message: 'Please, specify your secret_access_key_id' }]}
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Input.Password
|
||||
maxLength={40}
|
||||
visibilityToggle={secretKeyVisibility}
|
||||
onChange={() => setSecretKeyVisibility(true)}
|
||||
onFocus={() => onFocusCredentialsItem('secretKey', 'secret_key')}
|
||||
onBlur={() => onBlurCredentialsItem('secretKey', 'secret_key', setSecretKeyVisibility)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providerType === ProviderType.AZURE_CONTAINER &&
|
||||
credentialsType === CredentialsType.ACCOUNT_NAME_TOKEN_PAIR
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label='Account name'
|
||||
name='account_name'
|
||||
rules={[{ required: true, message: 'Please, specify your account name' }]}
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Input.Password
|
||||
minLength={3}
|
||||
maxLength={24}
|
||||
visibilityToggle={accountNameVisibility}
|
||||
onChange={() => setAccountNameVisibility(true)}
|
||||
onFocus={() => onFocusCredentialsItem('accountName', 'account_name')}
|
||||
onBlur={() =>
|
||||
onBlurCredentialsItem('accountName', 'account_name', setAccountNameVisibility)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label='SAS token'
|
||||
name='SAS_token'
|
||||
rules={[{ required: true, message: 'Please, specify your SAS token' }]}
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Input.Password
|
||||
visibilityToggle={sessionTokenVisibility}
|
||||
maxLength={437}
|
||||
onChange={() => setSessionTokenVisibility(true)}
|
||||
onFocus={() => onFocusCredentialsItem('sessionToken', 'session_token')}
|
||||
onBlur={() =>
|
||||
onBlurCredentialsItem('sessionToken', 'session_token', setSessionTokenVisibility)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (providerType === ProviderType.AZURE_CONTAINER && credentialsType === CredentialsType.ANONYMOUS_ACCESS) {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label='Account name'
|
||||
name='account_name'
|
||||
rules={[{ required: true, message: 'Please, specify your account name' }]}
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Input.Password
|
||||
minLength={3}
|
||||
maxLength={24}
|
||||
visibilityToggle={accountNameVisibility}
|
||||
onChange={() => setAccountNameVisibility(true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const AWSS3Configuration = (): JSX.Element => {
|
||||
const internalCommonProps = {
|
||||
...commonProps,
|
||||
labelCol: { span: 6, offset: 1 },
|
||||
wrapperCol: { offset: 1 },
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label='Bucket name'
|
||||
name='resource'
|
||||
rules={[{ required: true, message: 'Please, specify a bucket name' }]}
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Input disabled={!!cloudStorage} maxLength={63} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label='Authorization type'
|
||||
name='credentials_type'
|
||||
rules={[{ required: true, message: 'Please, specify credentials type' }]}
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Select onSelect={(value: CredentialsType) => onChangeCredentialsType(value)}>
|
||||
<Select.Option value={CredentialsType.KEY_SECRET_KEY_PAIR}>
|
||||
Key id and secret access key pair
|
||||
</Select.Option>
|
||||
<Select.Option value={CredentialsType.ANONYMOUS_ACCESS}>Anonymous access</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{credentialsBlok()}
|
||||
<S3Region
|
||||
selectedRegion={selectedRegion}
|
||||
onSelectRegion={onSelectRegion}
|
||||
internalCommonProps={internalCommonProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AzureBlobStorageConfiguration = (): JSX.Element => {
|
||||
const internalCommonProps = {
|
||||
...commonProps,
|
||||
labelCol: { span: 6, offset: 1 },
|
||||
wrapperCol: { offset: 1 },
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label='Container name'
|
||||
name='resource'
|
||||
rules={[{ required: true, message: 'Please, specify a container name' }]}
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Input disabled={!!cloudStorage} maxLength={63} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label='Authorization type'
|
||||
name='credentials_type'
|
||||
rules={[{ required: true, message: 'Please, specify credentials type' }]}
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Select onSelect={(value: CredentialsType) => onChangeCredentialsType(value)}>
|
||||
<Select.Option value={CredentialsType.ACCOUNT_NAME_TOKEN_PAIR}>
|
||||
Account name and SAS token
|
||||
</Select.Option>
|
||||
<Select.Option value={CredentialsType.ANONYMOUS_ACCESS}>Anonymous access</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{credentialsBlok()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form className='cvat-cloud-storage-form' layout='horizontal' form={form}>
|
||||
<Form.Item
|
||||
{...commonProps}
|
||||
label='Display name'
|
||||
name='display_name'
|
||||
rules={[{ required: true, message: 'Please, specify a display name' }]}
|
||||
>
|
||||
<Input maxLength={63} />
|
||||
</Form.Item>
|
||||
<Form.Item {...commonProps} label='Description' name='description'>
|
||||
<TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder='Any useful description' />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...commonProps}
|
||||
label='Provider'
|
||||
name='provider_type'
|
||||
rules={[{ required: true, message: 'Please, specify a cloud storage provider' }]}
|
||||
>
|
||||
<Select
|
||||
disabled={!!cloudStorage}
|
||||
onSelect={(value: ProviderType) => {
|
||||
setProviderType(value);
|
||||
setCredentialsType(null);
|
||||
form.resetFields(['credentials_type']);
|
||||
}}
|
||||
>
|
||||
<Select.Option value={ProviderType.AWS_S3_BUCKET}>
|
||||
<span className='cvat-cloud-storage-select-provider'>
|
||||
<S3Provider />
|
||||
AWS S3
|
||||
</span>
|
||||
</Select.Option>
|
||||
<Select.Option value={ProviderType.AZURE_CONTAINER}>
|
||||
<span className='cvat-cloud-storage-select-provider'>
|
||||
<AzureProvider />
|
||||
Azure Blob Container
|
||||
</span>
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{providerType === ProviderType.AWS_S3_BUCKET && AWSS3Configuration()}
|
||||
{providerType === ProviderType.AZURE_CONTAINER && AzureBlobStorageConfiguration()}
|
||||
<ManifestsManager form={form} manifestNames={manifestNames} setManifestNames={setManifestNames} />
|
||||
<Row justify='end'>
|
||||
<Col>
|
||||
<Button
|
||||
htmlType='button'
|
||||
onClick={() => onCancel()}
|
||||
className='cvat-cloud-storage-reset-button'
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Col>
|
||||
<Col offset={1}>
|
||||
<Button
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
onClick={onSubmit}
|
||||
className='cvat-cloud-storage-submit-button'
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{cloudStorage ? 'Update' : 'Submit'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React from 'react';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import CreateCloudStorageForm from './cloud-storage-form';
|
||||
|
||||
export default function CreateCloudStoragePageComponent(): JSX.Element {
|
||||
return (
|
||||
<Row justify='center' align='top' className='cvat-attach-cloud-storage-form-wrapper'>
|
||||
<Col md={20} lg={16} xl={14} xxl={9}>
|
||||
<Text className='cvat-title'>Create a cloud storage</Text>
|
||||
<CreateCloudStorageForm />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { MinusCircleOutlined, PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import Button from 'antd/lib/button';
|
||||
import Col from 'antd/lib/col';
|
||||
import Form from 'antd/lib/form';
|
||||
import Input from 'antd/lib/input';
|
||||
import Row from 'antd/lib/row';
|
||||
import notification from 'antd/lib/notification';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
|
||||
interface Props {
|
||||
form: any;
|
||||
manifestNames: string[];
|
||||
setManifestNames: (manifestNames: string[]) => void;
|
||||
}
|
||||
|
||||
export default function ManifestsManager(props: Props): JSX.Element {
|
||||
const { form, manifestNames, setManifestNames } = props;
|
||||
const maxManifestsCount = useRef(5);
|
||||
const [limitingAddingManifestNotification, setLimitingAddingManifestNotification] = useState(false);
|
||||
|
||||
const updateManifestFields = (): void => {
|
||||
const newManifestFormItems = manifestNames.map((name, idx) => ({
|
||||
id: idx,
|
||||
name,
|
||||
}));
|
||||
form.setFieldsValue({
|
||||
manifests: [...newManifestFormItems],
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateManifestFields();
|
||||
}, [manifestNames]);
|
||||
|
||||
useEffect(() => {
|
||||
if (limitingAddingManifestNotification) {
|
||||
notification.warning({
|
||||
message: `Unable to add manifest. The maximum number of files is ${maxManifestsCount.current}`,
|
||||
className: 'cvat-notification-limiting-adding-manifest',
|
||||
});
|
||||
}
|
||||
}, [limitingAddingManifestNotification]);
|
||||
|
||||
const onChangeManifestPath = (manifestName: string | undefined, manifestId: number): void => {
|
||||
if (manifestName !== undefined) {
|
||||
setManifestNames(manifestNames.map((name, idx) => (idx !== manifestId ? name : manifestName)));
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteManifestItem = (key: number): void => {
|
||||
if (maxManifestsCount.current === manifestNames.length && limitingAddingManifestNotification) {
|
||||
setLimitingAddingManifestNotification(false);
|
||||
}
|
||||
setManifestNames(manifestNames.filter((name, idx) => idx !== key));
|
||||
};
|
||||
|
||||
const onAddManifestItem = (): void => {
|
||||
if (maxManifestsCount.current <= manifestNames.length) {
|
||||
setLimitingAddingManifestNotification(true);
|
||||
} else {
|
||||
setManifestNames(manifestNames.concat(['']));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name='manifests'
|
||||
label={(
|
||||
<>
|
||||
Manifests
|
||||
<Tooltip title='More information'>
|
||||
<Button
|
||||
type='link'
|
||||
target='_blank'
|
||||
className='cvat-cloud-storage-help-button'
|
||||
href='https://openvinotoolkit.github.io/cvat/docs/manual/advanced/dataset_manifest/'
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
rules={[{ required: true, message: 'Please, specify at least one manifest file' }]}
|
||||
/>
|
||||
<Form.List name='manifests'>
|
||||
{
|
||||
(fields) => (
|
||||
<>
|
||||
{fields.map((field, idx): JSX.Element => (
|
||||
<Form.Item key={idx} shouldUpdate>
|
||||
<Row justify='space-between' align='top'>
|
||||
<Col>
|
||||
<Form.Item
|
||||
name={[idx, 'name']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please specify a manifest name',
|
||||
},
|
||||
]}
|
||||
initialValue={field.name}
|
||||
>
|
||||
<Input
|
||||
placeholder='manifest.jsonl'
|
||||
onChange={(event) => onChangeManifestPath(event.target.value, idx)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Item>
|
||||
<Button type='link' onClick={() => onDeleteManifestItem(idx)}>
|
||||
<MinusCircleOutlined />
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Form.List>
|
||||
<Row justify='start'>
|
||||
<Col>
|
||||
<Button type='ghost' onClick={onAddManifestItem} className='cvat-add-manifest-button'>
|
||||
Add manifest
|
||||
<PlusCircleOutlined />
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Divider from 'antd/lib/divider';
|
||||
import Select from 'antd/lib/select';
|
||||
import { PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import Input from 'antd/lib/input';
|
||||
import Button from 'antd/lib/button';
|
||||
import Form from 'antd/lib/form';
|
||||
import notification from 'antd/lib/notification';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import consts from '../../consts';
|
||||
|
||||
const { Option } = Select;
|
||||
interface Props {
|
||||
selectedRegion: undefined | string;
|
||||
onSelectRegion: any;
|
||||
internalCommonProps: any;
|
||||
}
|
||||
|
||||
function prepareDefaultRegions(): Map<string, string> {
|
||||
const temp = new Map<string, string>();
|
||||
for (const [key, value] of consts.DEFAULT_AWS_S3_REGIONS) {
|
||||
temp.set(key, value);
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
export default function S3Region(props: Props): JSX.Element {
|
||||
const { selectedRegion, onSelectRegion, internalCommonProps } = props;
|
||||
const [regions, setRegions] = useState<Map<string, string>>(() => prepareDefaultRegions());
|
||||
const [newRegionKey, setNewRegionKey] = useState<string>('');
|
||||
const [newRegionName, setNewRegionName] = useState<string>('');
|
||||
|
||||
const handleAddingRegion = (): void => {
|
||||
if (!newRegionKey || !newRegionName) {
|
||||
notification.warning({
|
||||
message: 'Incorrect region',
|
||||
className: 'cvat-incorrect-add-region-notification',
|
||||
});
|
||||
} else if (regions.has(newRegionKey)) {
|
||||
notification.warning({
|
||||
message: 'This region already exists',
|
||||
className: 'cvat-incorrect-add-region-notification',
|
||||
});
|
||||
} else {
|
||||
const regionsCopy = regions;
|
||||
setRegions(regionsCopy.set(newRegionKey, newRegionName));
|
||||
setNewRegionKey('');
|
||||
setNewRegionName('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label={(
|
||||
<>
|
||||
Region
|
||||
<Tooltip title='More information'>
|
||||
<Button
|
||||
className='cvat-cloud-storage-help-button'
|
||||
type='link'
|
||||
target='_blank'
|
||||
href='https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions'
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
name='region'
|
||||
{...internalCommonProps}
|
||||
>
|
||||
<Select
|
||||
placeholder='Select region'
|
||||
defaultValue={selectedRegion ? regions.get(selectedRegion) : undefined}
|
||||
dropdownRender={(menu) => (
|
||||
<div>
|
||||
{menu}
|
||||
<Divider className='cvat-divider' />
|
||||
<div className='cvat-cloud-storage-region-creator'>
|
||||
<Input
|
||||
value={newRegionKey}
|
||||
onChange={(event: any) => setNewRegionKey(event.target.value)}
|
||||
maxLength={14}
|
||||
placeholder='key'
|
||||
/>
|
||||
<Input
|
||||
value={newRegionName}
|
||||
onChange={(event: any) => setNewRegionName(event.target.value)}
|
||||
placeholder='name'
|
||||
/>
|
||||
<Button
|
||||
type='link'
|
||||
onClick={handleAddingRegion}
|
||||
>
|
||||
Add region
|
||||
<PlusCircleOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
onSelect={(_, instance) => onSelectRegion(instance.key)}
|
||||
>
|
||||
{
|
||||
Array.from(regions.entries()).map(
|
||||
([key, value]): JSX.Element => (
|
||||
<Option key={key} value={value}>
|
||||
{value}
|
||||
</Option>
|
||||
),
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-update-cloud-storage-form-wrapper,
|
||||
.cvat-attach-cloud-storage-form-wrapper {
|
||||
text-align: center;
|
||||
padding-top: $grid-unit-size * 5;
|
||||
overflow-y: auto;
|
||||
height: 90%;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
|
||||
> div > span {
|
||||
font-size: $grid-unit-size * 4;
|
||||
}
|
||||
|
||||
.cvat-cloud-storage-form {
|
||||
margin-top: $grid-unit-size * 2;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid $border-color-1;
|
||||
border-radius: $grid-unit-size / 2;
|
||||
padding: $grid-unit-size * 2;
|
||||
background: $background-color-1;
|
||||
text-align: initial;
|
||||
|
||||
.cvat-cloud-storage-form-item {
|
||||
justify-content: space-between;
|
||||
|
||||
> div:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
> div:not(first-child) {
|
||||
margin-top: $grid-unit-size;
|
||||
}
|
||||
|
||||
.cvat-attach-cloud-storage-reset-button,
|
||||
.cvat-attach-cloud-storage-submit-button {
|
||||
width: $grid-unit-size * 12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-cloud-storage-region-creator {
|
||||
display: flex;
|
||||
padding: $grid-unit-size;
|
||||
|
||||
> * {
|
||||
margin: 0 $grid-unit-size;
|
||||
}
|
||||
|
||||
> button {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-cloud-storage-help-button {
|
||||
padding-left: $grid-unit-size * 0.5;
|
||||
padding-right: 0;
|
||||
}
|
||||
@ -0,0 +1,239 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React, { ReactText, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Row } from 'antd/lib/grid';
|
||||
import Tree from 'antd/lib/tree/Tree';
|
||||
import Spin from 'antd/lib/spin';
|
||||
import Alert from 'antd/lib/alert';
|
||||
import Empty from 'antd/lib/empty';
|
||||
import { EventDataNode } from 'antd/lib/tree';
|
||||
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
||||
import Divider from 'antd/lib/divider';
|
||||
import { CloudStorage, CombinedState } from 'reducers/interfaces';
|
||||
import { loadCloudStorageContentAsync } from 'actions/cloud-storage-actions';
|
||||
|
||||
interface Props {
|
||||
cloudStorage: CloudStorage;
|
||||
selectedManifest: string;
|
||||
onSelectFiles: (checkedKeysValue: string[]) => void;
|
||||
selectedFiles: string[];
|
||||
}
|
||||
|
||||
interface DataNode {
|
||||
title: string;
|
||||
key: string;
|
||||
isLeaf: boolean;
|
||||
disabled: boolean;
|
||||
children: DataNode[];
|
||||
}
|
||||
|
||||
interface DataStructure {
|
||||
name: string;
|
||||
children: Map<string, DataStructure> | null;
|
||||
unparsedChildren: string[] | null;
|
||||
}
|
||||
|
||||
type Files =
|
||||
| ReactText[]
|
||||
| {
|
||||
checked: ReactText[];
|
||||
halfChecked: ReactText[];
|
||||
};
|
||||
|
||||
export default function CloudStorageFiles(props: Props): JSX.Element {
|
||||
const {
|
||||
cloudStorage, selectedManifest, selectedFiles, onSelectFiles,
|
||||
} = props;
|
||||
const dispatch = useDispatch();
|
||||
const isFetching = useSelector((state: CombinedState) => state.cloudStorages.activities.contentLoads.fetching);
|
||||
const content = useSelector((state: CombinedState) => state.cloudStorages.activities.contentLoads.content);
|
||||
const error = useSelector((state: CombinedState) => state.cloudStorages.activities.contentLoads.error);
|
||||
const [treeData, setTreeData] = useState<DataNode[]>([]);
|
||||
const [initialData, setInitialData] = useState<DataStructure>({
|
||||
name: 'root',
|
||||
children: null,
|
||||
unparsedChildren: null,
|
||||
});
|
||||
|
||||
const [checkedAll, setCheckedAll] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(loadCloudStorageContentAsync(cloudStorage));
|
||||
}, [cloudStorage.id, selectedManifest]);
|
||||
|
||||
const parseContent = (mass: string[], root = ''): Map<string, DataStructure> => {
|
||||
const data: Map<string, DataStructure> = new Map();
|
||||
// define directories
|
||||
const upperDirs: Set<string> = new Set(
|
||||
mass.filter((path: string) => path.includes('/')).map((path: string) => path.split('/', 1)[0]),
|
||||
);
|
||||
|
||||
for (const dir of upperDirs) {
|
||||
const child: DataStructure = {
|
||||
name: dir,
|
||||
children: null,
|
||||
unparsedChildren: mass
|
||||
.filter((path: string) => path.startsWith(`${dir}/`))
|
||||
.map((path: string) => path.replace(`${dir}/`, '')),
|
||||
};
|
||||
data.set(`${root}${dir}/`, child);
|
||||
}
|
||||
|
||||
// define files
|
||||
const rootFiles = mass.filter((path: string) => !path.includes('/'));
|
||||
|
||||
for (const rootFile of rootFiles) {
|
||||
const child: DataStructure = {
|
||||
name: rootFile,
|
||||
children: null,
|
||||
unparsedChildren: null,
|
||||
};
|
||||
data.set(`${root}${rootFile}`, child);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const updateData = (key: string, data: Map<string, DataStructure> | null): Map<string, DataStructure> | null => {
|
||||
if (data === null) {
|
||||
return data;
|
||||
}
|
||||
|
||||
for (const [dataItemKey, dataItemValue] of data) {
|
||||
if (key.startsWith(dataItemKey) && key.replace(dataItemKey, '')) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
data = updateData(key, dataItemValue.children);
|
||||
} else if (dataItemKey === key) {
|
||||
const unparsedDataItemChildren = dataItemValue.unparsedChildren;
|
||||
if (dataItemValue && unparsedDataItemChildren) {
|
||||
dataItemValue.children = parseContent(unparsedDataItemChildren, dataItemKey);
|
||||
dataItemValue.unparsedChildren = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const onLoadData = (key: string): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
if (initialData.children === null) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
setInitialData({
|
||||
...initialData,
|
||||
children: updateData(key, initialData.children),
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (content) {
|
||||
const children = parseContent(content);
|
||||
|
||||
setInitialData({
|
||||
...initialData,
|
||||
children,
|
||||
});
|
||||
} else {
|
||||
setInitialData({
|
||||
name: 'root',
|
||||
children: null,
|
||||
unparsedChildren: null,
|
||||
});
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
const prepareNodes = (data: Map<string, DataStructure>, nodes: DataNode[]): DataNode[] => {
|
||||
for (const [key, value] of data) {
|
||||
const node: DataNode = {
|
||||
title: value.name,
|
||||
key,
|
||||
isLeaf: !value.children && !value.unparsedChildren,
|
||||
disabled: !!value.unparsedChildren,
|
||||
children: [],
|
||||
};
|
||||
if (value.children) {
|
||||
node.children = prepareNodes(value.children, []);
|
||||
}
|
||||
nodes.push(node);
|
||||
}
|
||||
return nodes;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData.children && content) {
|
||||
const nodes = prepareNodes(initialData.children, []);
|
||||
setTreeData(nodes);
|
||||
} else {
|
||||
setTreeData([]);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const onChangeCheckedAll = (checked: boolean): void => {
|
||||
setCheckedAll(checked);
|
||||
if (checked) {
|
||||
onSelectFiles((content as string[]).concat([selectedManifest]));
|
||||
} else {
|
||||
onSelectFiles([]);
|
||||
}
|
||||
};
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<Row className='cvat-create-task-page-empty-cloud-storage' justify='center' align='middle'>
|
||||
<Spin size='large' />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert
|
||||
className='cvat-cloud-storage-alert-fetching-failed'
|
||||
message='Could not fetch cloud storage data'
|
||||
type='error'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{treeData.length ? (
|
||||
<>
|
||||
<Checkbox
|
||||
className='cvat-cloud-storage-files-checkbox'
|
||||
onChange={(event: CheckboxChangeEvent) => onChangeCheckedAll(event.target.checked)}
|
||||
checked={checkedAll}
|
||||
>
|
||||
Select all
|
||||
</Checkbox>
|
||||
<Divider className='cvat-divider' />
|
||||
<Tree.DirectoryTree
|
||||
selectable={false}
|
||||
multiple
|
||||
checkable
|
||||
height={256}
|
||||
onCheck={(checkedKeys: Files) => {
|
||||
const checkedFiles = (checkedKeys as string[]).filter((f) => !f.endsWith('/'));
|
||||
if (checkedFiles.length === (content as string[]).length) {
|
||||
setCheckedAll(true);
|
||||
} else if (checkedAll) {
|
||||
setCheckedAll(false);
|
||||
}
|
||||
onSelectFiles(checkedFiles.concat([selectedManifest]));
|
||||
}}
|
||||
loadData={(event: EventDataNode): Promise<void> => onLoadData(event.key.toLocaleString())}
|
||||
treeData={treeData}
|
||||
checkedKeys={selectedFiles}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty className='cvat-empty-cloud-storages-tree' description='The storage is empty' />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Form from 'antd/lib/form';
|
||||
import notification from 'antd/lib/notification';
|
||||
import AutoComplete from 'antd/lib/auto-complete';
|
||||
import Input from 'antd/lib/input';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import Select from 'antd/lib/select';
|
||||
import getCore from 'cvat-core-wrapper';
|
||||
import { CloudStorage } from 'reducers/interfaces';
|
||||
import { AzureProvider, S3Provider } from 'icons';
|
||||
import { ProviderType } from 'utils/enums';
|
||||
import CloudStorageFiles from './cloud-storages-files';
|
||||
|
||||
interface Props {
|
||||
formRef: any;
|
||||
cloudStorage: CloudStorage | null;
|
||||
searchPhrase: string;
|
||||
setSearchPhrase: (searchPhrase: string) => void;
|
||||
selectedFiles: string[];
|
||||
onSelectFiles: (files: 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 { Option } = Select;
|
||||
|
||||
const searchCloudStoragesWrapper = debounce((phrase, setList) => {
|
||||
const filter = { displayName: phrase };
|
||||
searchCloudStorages(filter).then((list) => {
|
||||
setList(list);
|
||||
});
|
||||
}, 500);
|
||||
|
||||
export default function CloudStorageTab(props: Props): JSX.Element {
|
||||
const { searchPhrase, setSearchPhrase } = props;
|
||||
const [initialList, setInitialList] = useState<CloudStorage[]>([]);
|
||||
const [list, setList] = useState<CloudStorage[]>([]);
|
||||
const {
|
||||
formRef, cloudStorage, selectedFiles, onSelectFiles, onSelectCloudStorage,
|
||||
} = props;
|
||||
const [selectedManifest, setSelectedManifest] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
searchCloudStorages({}).then((data) => {
|
||||
setInitialList(data);
|
||||
if (!list.length) {
|
||||
setList(data);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchPhrase) {
|
||||
setList(initialList);
|
||||
} else {
|
||||
searchCloudStoragesWrapper(searchPhrase, setList);
|
||||
}
|
||||
}, [searchPhrase, initialList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cloudStorage) {
|
||||
setSelectedManifest(cloudStorage.manifests[0]);
|
||||
}
|
||||
}, [cloudStorage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedManifest) {
|
||||
cloudStorage.manifestPath = selectedManifest;
|
||||
}
|
||||
}, [selectedManifest]);
|
||||
|
||||
const onBlur = (): void => {
|
||||
if (!searchPhrase && cloudStorage) {
|
||||
onSelectCloudStorage(null);
|
||||
} else if (searchPhrase) {
|
||||
const potentialStorages = list.filter((_cloudStorage) => _cloudStorage.displayName.includes(searchPhrase));
|
||||
if (potentialStorages.length === 1) {
|
||||
const potentialStorage = potentialStorages[0];
|
||||
setSearchPhrase(potentialStorage.displayName);
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
potentialStorage.manifestPath = potentialStorage.manifests[0];
|
||||
onSelectCloudStorage(potentialStorage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form ref={formRef} className='cvat-create-task-page-cloud-storages-tab-form' layout='vertical'>
|
||||
<Form.Item
|
||||
label='Select cloud storage'
|
||||
name='cloudStorageSelect'
|
||||
rules={[{ required: true, message: 'Please, specify a cloud storage' }]}
|
||||
valuePropName='label'
|
||||
>
|
||||
<AutoComplete
|
||||
onBlur={onBlur}
|
||||
value={searchPhrase}
|
||||
placeholder='Search...'
|
||||
showSearch
|
||||
onSearch={(phrase: string) => {
|
||||
setSearchPhrase(phrase);
|
||||
}}
|
||||
options={list.map((_cloudStorage) => ({
|
||||
value: _cloudStorage.id.toString(),
|
||||
label: (
|
||||
<span
|
||||
className='cvat-cloud-storage-select-provider'
|
||||
>
|
||||
{_cloudStorage.providerType === ProviderType.AWS_S3_BUCKET ? (
|
||||
<S3Provider />
|
||||
) : (
|
||||
<AzureProvider />
|
||||
)}
|
||||
{_cloudStorage.displayName}
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
onSelect={(value: string) => {
|
||||
const selectedCloudStorage =
|
||||
list.filter((_cloudStorage: CloudStorage) => _cloudStorage.id === +value)[0] || null;
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
selectedCloudStorage.manifestPath = selectedCloudStorage.manifests[0];
|
||||
onSelectCloudStorage(selectedCloudStorage);
|
||||
setSearchPhrase(selectedCloudStorage?.displayName || '');
|
||||
}}
|
||||
allowClear
|
||||
>
|
||||
<Input />
|
||||
</AutoComplete>
|
||||
</Form.Item>
|
||||
|
||||
{cloudStorage ? (
|
||||
<Form.Item
|
||||
label='Select manifest file'
|
||||
name='manifestSelect'
|
||||
rules={[{ required: true, message: 'Please, specify a manifest file' }]}
|
||||
initialValue={cloudStorage.manifests[0]}
|
||||
>
|
||||
<Select
|
||||
onSelect={(value: string) => setSelectedManifest(value)}
|
||||
>
|
||||
{cloudStorage.manifests.map(
|
||||
(manifest: string): JSX.Element => (
|
||||
<Option key={manifest} value={manifest}>
|
||||
{manifest}
|
||||
</Option>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
) : null}
|
||||
|
||||
{cloudStorage && selectedManifest ? (
|
||||
<Form.Item
|
||||
label='Files'
|
||||
name='cloudStorageFiles'
|
||||
rules={[{ required: true, message: 'Please, select a files' }]}
|
||||
>
|
||||
<CloudStorageFiles
|
||||
cloudStorage={cloudStorage}
|
||||
selectedManifest={selectedManifest}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectFiles={onSelectFiles}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : null}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
// Copyright (C) 2020-2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Search from 'antd/lib/input/Search';
|
||||
|
||||
import SearchTooltip from 'components/search-tooltip/search-tooltip';
|
||||
import { CombinedState, ProjectsQuery } from 'reducers/interfaces';
|
||||
import { getProjectsAsync } from 'actions/projects-actions';
|
||||
|
||||
function getSearchField(gettingQuery: ProjectsQuery): string {
|
||||
let searchString = '';
|
||||
for (const field of Object.keys(gettingQuery)) {
|
||||
if (gettingQuery[field] !== null && field !== 'page') {
|
||||
if (field === 'search') {
|
||||
return (gettingQuery[field] as any) as string;
|
||||
}
|
||||
|
||||
// not constant condition
|
||||
// eslint-disable-next-line
|
||||
if (typeof (gettingQuery[field] === 'number')) {
|
||||
searchString += `${field}:${gettingQuery[field]} AND `;
|
||||
} else {
|
||||
searchString += `${field}:"${gettingQuery[field]}" AND `;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return searchString.slice(0, -5);
|
||||
}
|
||||
|
||||
export default function ProjectSearchField(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
|
||||
|
||||
const handleSearch = (value: string): void => {
|
||||
const query = { ...gettingQuery };
|
||||
const search = value
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\s*:+\s*/g, ':')
|
||||
.trim();
|
||||
|
||||
const fields = Object.keys(query).filter((key) => key !== 'page');
|
||||
for (const field of fields) {
|
||||
query[field] = null;
|
||||
}
|
||||
query.search = null;
|
||||
|
||||
let specificRequest = false;
|
||||
for (const param of search.split(/[\s]+and[\s]+|[\s]+AND[\s]+/)) {
|
||||
if (param.includes(':')) {
|
||||
const [field, fieldValue] = param.split(':');
|
||||
if (fields.includes(field) && !!fieldValue) {
|
||||
specificRequest = true;
|
||||
if (field === 'id') {
|
||||
if (Number.isInteger(+fieldValue)) {
|
||||
query[field] = +fieldValue;
|
||||
}
|
||||
} else {
|
||||
query[field] = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.page = 1;
|
||||
if (!specificRequest && value) {
|
||||
query.search = value;
|
||||
}
|
||||
|
||||
dispatch(getProjectsAsync(query));
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchTooltip instance='project'>
|
||||
<Search
|
||||
defaultValue={getSearchField(gettingQuery)}
|
||||
onSearch={handleSearch}
|
||||
size='large'
|
||||
placeholder='Search'
|
||||
/>
|
||||
</SearchTooltip>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React from 'react';
|
||||
import Search from 'antd/lib/input/Search';
|
||||
import SearchTooltip from 'components/search-tooltip/search-tooltip';
|
||||
|
||||
interface Query {
|
||||
[key: string]: string | number | boolean | null | undefined;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
query: Query;
|
||||
instance: 'task' | 'project' | 'cloudstorage';
|
||||
onSearch(query: object): void;
|
||||
}
|
||||
|
||||
export default function SearchField(props: Props): JSX.Element {
|
||||
const { onSearch, query, instance } = props;
|
||||
function parse(_query: Query): string {
|
||||
let searchString = '';
|
||||
for (const field of Object.keys(_query)) {
|
||||
const value = _query[field];
|
||||
if (value !== null && typeof value !== 'undefined' && field !== 'page') {
|
||||
if (field === 'search') {
|
||||
return _query[field] as string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
if (typeof (_query[field] === 'number')) {
|
||||
searchString += `${field}: ${_query[field]} AND `;
|
||||
} else {
|
||||
searchString += `${field}: "${_query[field]}" AND `;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return searchString.slice(0, -5);
|
||||
}
|
||||
|
||||
const handleSearch = (value: string): void => {
|
||||
const currentQuery = { ...query };
|
||||
const search = value
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\s*:+\s*/g, ':')
|
||||
.trim();
|
||||
|
||||
const fields = Object.keys(query).filter((key) => key !== 'page');
|
||||
for (const field of fields) {
|
||||
currentQuery[field] = null;
|
||||
}
|
||||
currentQuery.search = null;
|
||||
|
||||
let specificRequest = false;
|
||||
for (const param of search.split(/[\s]+and[\s]+|[\s]+AND[\s]+/)) {
|
||||
if (param.includes(':')) {
|
||||
const [field, fieldValue] = param.split(':');
|
||||
if (fields.includes(field) && !!fieldValue) {
|
||||
specificRequest = true;
|
||||
if (field === 'id') {
|
||||
if (Number.isInteger(+fieldValue)) {
|
||||
currentQuery[field] = +fieldValue;
|
||||
}
|
||||
} else {
|
||||
currentQuery[field] = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentQuery.page = 1;
|
||||
if (!specificRequest && value) {
|
||||
currentQuery.search = value;
|
||||
}
|
||||
|
||||
onSearch(currentQuery);
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchTooltip instance={instance}>
|
||||
<Search
|
||||
className='cvat-search-field'
|
||||
defaultValue={parse(query)}
|
||||
onSearch={handleSearch}
|
||||
size='large'
|
||||
placeholder='Search'
|
||||
/>
|
||||
</SearchTooltip>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-search-field {
|
||||
width: $grid-unit-size * 30;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-update-cloud-storage-form-wrapper {
|
||||
text-align: center;
|
||||
padding-top: $grid-unit-size * 5;
|
||||
overflow-y: auto;
|
||||
height: 90%;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
|
||||
> div > span {
|
||||
font-size: $grid-unit-size * 4;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Spin from 'antd/lib/spin';
|
||||
import Result from 'antd/lib/result';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import { getCloudStoragesAsync } from 'actions/cloud-storage-actions';
|
||||
import CreateCloudStorageForm from 'components/create-cloud-storage-page/cloud-storage-form';
|
||||
|
||||
interface ParamType {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default function UpdateCloudStoragePageComponent(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const cloudStorageId = +useParams<ParamType>().id;
|
||||
const isFetching = useSelector((state: CombinedState) => state.cloudStorages.fetching);
|
||||
const isInitialized = useSelector((state: CombinedState) => state.cloudStorages.initialized);
|
||||
const cloudStorages = useSelector((state: CombinedState) => state.cloudStorages.current)
|
||||
.map((cloudStrage) => cloudStrage.instance);
|
||||
const [cloudStorage] = cloudStorages.filter((_cloudStorage) => _cloudStorage.id === cloudStorageId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cloudStorage && !isFetching) {
|
||||
dispatch(getCloudStoragesAsync({ id: cloudStorageId }));
|
||||
}
|
||||
}, [isFetching]);
|
||||
|
||||
if (!cloudStorage && !isInitialized) {
|
||||
return <Spin size='large' className='cvat-spinner' />;
|
||||
}
|
||||
|
||||
if (!cloudStorage) {
|
||||
return (
|
||||
<Result
|
||||
className='cvat-not-found'
|
||||
status='404'
|
||||
title={`Sorry, but the cloud storage #${cloudStorageId} was not found`}
|
||||
subTitle='Please, be sure id you requested exists and you have appropriate permissions'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row justify='center' align='top' className='cvat-update-cloud-storage-form-wrapper'>
|
||||
<Col md={20} lg={16} xl={14} xxl={9}>
|
||||
<Text className='cvat-title'>{`Update cloud storage #${cloudStorageId}`}</Text>
|
||||
<CreateCloudStorageForm cloudStorage={cloudStorage} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,343 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { CloudStorageActions, CloudStorageActionTypes } from 'actions/cloud-storage-actions';
|
||||
import { AuthActions, AuthActionTypes } from 'actions/auth-actions';
|
||||
import { CloudStoragesState, CloudStorage } from './interfaces';
|
||||
|
||||
const defaultState: CloudStoragesState = {
|
||||
initialized: false,
|
||||
fetching: false,
|
||||
count: 0,
|
||||
current: [],
|
||||
// currentStatuses: [],
|
||||
gettingQuery: {
|
||||
page: 1,
|
||||
id: null,
|
||||
search: null,
|
||||
owner: null,
|
||||
displayName: null,
|
||||
description: null,
|
||||
resourceName: null,
|
||||
providerType: null,
|
||||
credentialsType: null,
|
||||
status: null,
|
||||
},
|
||||
activities: {
|
||||
creates: {
|
||||
attaching: false,
|
||||
id: null,
|
||||
error: '',
|
||||
},
|
||||
updates: {
|
||||
updating: false,
|
||||
cloudStorageID: null,
|
||||
error: '',
|
||||
},
|
||||
deletes: {},
|
||||
contentLoads: {
|
||||
cloudStorageID: null,
|
||||
content: null,
|
||||
fetching: false,
|
||||
error: '',
|
||||
},
|
||||
// getsStatus: {
|
||||
// cloudStorageID: null,
|
||||
// status: null,
|
||||
// fetching: false,
|
||||
// error: '',
|
||||
// },
|
||||
},
|
||||
};
|
||||
|
||||
export default (
|
||||
state: CloudStoragesState = defaultState,
|
||||
action: CloudStorageActions | AuthActions,
|
||||
): CloudStoragesState => {
|
||||
switch (action.type) {
|
||||
case CloudStorageActionTypes.UPDATE_CLOUD_STORAGES_GETTING_QUERY:
|
||||
return {
|
||||
...state,
|
||||
gettingQuery: {
|
||||
...defaultState.gettingQuery,
|
||||
...action.payload.query,
|
||||
},
|
||||
};
|
||||
case CloudStorageActionTypes.GET_CLOUD_STORAGES:
|
||||
return {
|
||||
...state,
|
||||
initialized: false,
|
||||
fetching: true,
|
||||
count: 0,
|
||||
current: [],
|
||||
// currentStatuses: [],
|
||||
};
|
||||
case CloudStorageActionTypes.GET_CLOUD_STORAGE_SUCCESS: {
|
||||
const { count, query } = action.payload;
|
||||
|
||||
const combined = action.payload.array.map(
|
||||
(cloudStorage: any, index: number): CloudStorage => ({
|
||||
instance: cloudStorage,
|
||||
preview: action.payload.previews[index],
|
||||
status: action.payload.statuses[index],
|
||||
}),
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
initialized: true,
|
||||
fetching: false,
|
||||
count,
|
||||
gettingQuery: {
|
||||
...defaultState.gettingQuery,
|
||||
...query,
|
||||
},
|
||||
current: combined,
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.GET_CLOUD_STORAGE_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
initialized: true,
|
||||
fetching: false,
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.CREATE_CLOUD_STORAGE: {
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
creates: {
|
||||
attaching: true,
|
||||
id: null,
|
||||
error: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.CREATE_CLOUD_STORAGE_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
creates: {
|
||||
attaching: false,
|
||||
id: action.payload.cloudStorageID,
|
||||
error: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.CREATE_CLOUD_STORAGE_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
creates: {
|
||||
...state.activities.creates,
|
||||
attaching: false,
|
||||
error: action.payload.error.toString(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.UPDATE_CLOUD_STORAGE: {
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
updates: {
|
||||
updating: true,
|
||||
cloudStorageID: null,
|
||||
error: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.UPDATE_CLOUD_STORAGE_SUCCESS: {
|
||||
const { cloudStorage } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
updates: {
|
||||
updating: false,
|
||||
cloudStorageID: cloudStorage.id,
|
||||
error: '',
|
||||
},
|
||||
},
|
||||
current: state.current.map(
|
||||
(_cloudStorage: CloudStorage): CloudStorage => {
|
||||
if (_cloudStorage.id === cloudStorage.id) {
|
||||
return cloudStorage;
|
||||
}
|
||||
|
||||
return _cloudStorage;
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.UPDATE_CLOUD_STORAGE_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
updates: {
|
||||
...state.activities.updates,
|
||||
updating: false,
|
||||
error: action.payload.error.toString(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.DELETE_CLOUD_STORAGE: {
|
||||
const { cloudStorageID } = action.payload;
|
||||
const { deletes } = state.activities;
|
||||
|
||||
deletes[cloudStorageID] = false;
|
||||
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
deletes: {
|
||||
...deletes,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.DELETE_CLOUD_STORAGE_SUCCESS: {
|
||||
const { cloudStorageID } = action.payload;
|
||||
const { deletes } = state.activities;
|
||||
|
||||
deletes[cloudStorageID] = true;
|
||||
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
deletes: {
|
||||
...deletes,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.DELETE_CLOUD_STORAGE_FAILED: {
|
||||
const { cloudStorageID } = action.payload;
|
||||
const { deletes } = state.activities;
|
||||
|
||||
delete deletes[cloudStorageID];
|
||||
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
deletes: {
|
||||
...deletes,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.LOAD_CLOUD_STORAGE_CONTENT:
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
contentLoads: {
|
||||
cloudStorageID: null,
|
||||
content: null,
|
||||
error: '',
|
||||
fetching: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
case CloudStorageActionTypes.LOAD_CLOUD_STORAGE_CONTENT_SUCCESS: {
|
||||
const { cloudStorageID, content } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
contentLoads: {
|
||||
cloudStorageID,
|
||||
content,
|
||||
error: '',
|
||||
fetching: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case CloudStorageActionTypes.LOAD_CLOUD_STORAGE_CONTENT_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
activities: {
|
||||
...state.activities,
|
||||
contentLoads: {
|
||||
...state.activities.contentLoads,
|
||||
error: action.payload.error.toString(),
|
||||
fetching: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
// case CloudStorageActionTypes.GET_CLOUD_STORAGE_STATUS:
|
||||
// return {
|
||||
// ...state,
|
||||
// activities: {
|
||||
// ...state.activities,
|
||||
// getsStatus: {
|
||||
// cloudStorageID: null,
|
||||
// status: null,
|
||||
// error: '',
|
||||
// fetching: true,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// case CloudStorageActionTypes.GET_CLOUD_STORAGE_STATUS_SUCCESS: {
|
||||
// const { cloudStorageID, status } = action.payload;
|
||||
// const statuses = state.currentStatuses;
|
||||
// const index = statuses.findIndex((item) => item.id === cloudStorageID);
|
||||
// if (index !== -1) {
|
||||
// statuses[index] = {
|
||||
// ...statuses[index],
|
||||
// status,
|
||||
// };
|
||||
// } else {
|
||||
// statuses.push({
|
||||
// id: cloudStorageID,
|
||||
// status,
|
||||
// });
|
||||
// }
|
||||
// return {
|
||||
// ...state,
|
||||
// currentStatuses: statuses,
|
||||
// activities: {
|
||||
// ...state.activities,
|
||||
// getsStatus: {
|
||||
// cloudStorageID,
|
||||
// status,
|
||||
// error: '',
|
||||
// fetching: false,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
// case CloudStorageActionTypes.GET_CLOUD_STORAGE_STATUS_FAILED: {
|
||||
// return {
|
||||
// ...state,
|
||||
// activities: {
|
||||
// ...state.activities,
|
||||
// getsStatus: {
|
||||
// ...state.activities.getsStatus,
|
||||
// error: action.payload.error.toString(),
|
||||
// fetching: false,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
case AuthActionTypes.LOGOUT_SUCCESS: {
|
||||
return { ...defaultState };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export enum ProviderType {
|
||||
AWS_S3_BUCKET = 'AWS_S3_BUCKET',
|
||||
AZURE_CONTAINER = 'AZURE_CONTAINER',
|
||||
}
|
||||
|
||||
export enum CredentialsType {
|
||||
KEY_SECRET_KEY_PAIR = 'KEY_SECRET_KEY_PAIR',
|
||||
ACCOUNT_NAME_TOKEN_PAIR = 'ACCOUNT_NAME_TOKEN_PAIR',
|
||||
ANONYMOUS_ACCESS = 'ANONYMOUS_ACCESS',
|
||||
}
|
||||
|
||||
export enum StorageStatuses {
|
||||
AVAILABLE = 'AVAILABLE',
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
}
|
||||
Loading…
Reference in New Issue