// Copyright (C) 2021-2022 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 Tooltip from 'antd/lib/tooltip'; import { CombinedState, CloudStorage } from 'reducers/interfaces'; import { createCloudStorageAsync, updateCloudStorageAsync } from 'actions/cloud-storage-actions'; import { ProviderType, CredentialsType } from 'utils/enums'; import { QuestionCircleOutlined, UploadOutlined } from '@ant-design/icons'; import Upload, { RcFile } from 'antd/lib/upload'; import Space from 'antd/lib/space'; import { AzureProvider, S3Provider, GoogleCloudProvider } from '../../icons'; import S3Region from './s3-region'; import GCSLocation from './gcs-locatiion'; 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; key_file?: File; description?: string; region?: string; prefix?: string; project_id?: string; manifests: string[]; } const { Dragger } = Upload; export default function CreateCloudStorageForm(props: Props): JSX.Element { const { cloudStorage } = props; const cloudStorageId = cloudStorage ? cloudStorage.id : null; const dispatch = useDispatch(); const history = useHistory(); const [form] = Form.useForm(); const shouldShowCreationNotification = useRef(false); const shouldShowUpdationNotification = useRef(false); const [providerType, setProviderType] = useState(null); const [credentialsType, setCredentialsType] = useState(null); const [selectedRegion, setSelectedRegion] = useState(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), keyFile: new File([], 'fakeKey.json'), }; const [keyVisibility, setKeyVisibility] = useState(false); const [secretKeyVisibility, setSecretKeyVisibility] = useState(false); const [sessionTokenVisibility, setSessionTokenVisibility] = useState(false); const [accountNameVisibility, setAccountNameVisibility] = useState(false); const [manifestNames, setManifestNames] = useState([]); const [uploadedKeyFile, setUploadedKeyFile] = useState(null); const [isFakeKeyFileAttached, setIsFakeKeyFileAttached] = useState(!!cloudStorage); 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.resource, 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; } else if (cloudStorage.credentialsType === CredentialsType.KEY_FILE_PATH) { setUploadedKeyFile(fakeCredentialsData.keyFile); } if (cloudStorage.specificAttributes) { const parsedOptions = new URLSearchParams(cloudStorage.specificAttributes); const location = parsedOptions.get('region') || parsedOptions.get('location'); const prefix = parsedOptions.get('prefix'); const projectId = parsedOptions.get('project_id'); if (location) { setSelectedRegion(location); } if (prefix) { fieldsValue.prefix = prefix; } if (projectId) { fieldsValue.project_id = projectId; } } form.setFieldsValue(fieldsValue); } function onReset(): void { if (cloudStorage) { initializeFields(); } else { setManifestNames([]); setSelectedRegion(undefined); setUploadedKeyFile(null); 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 (cloudStorageId && 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, }); } }, []); const onSubmit = async (): Promise => { let cloudStorageData: Record = {}; const formValues = await form.validateFields(); cloudStorageData = { ...formValues }; // specific attributes const specificAttributes = new URLSearchParams(); if (selectedRegion) { if (cloudStorageData.provider_type === ProviderType.AWS_S3_BUCKET) { delete cloudStorageData.region; specificAttributes.append('region', selectedRegion as string); } else if (cloudStorageData.provider_type === ProviderType.GOOGLE_CLOUD_STORAGE) { delete cloudStorageData.location; specificAttributes.append('location', selectedRegion as string); } } if (formValues.prefix) { delete cloudStorageData.prefix; specificAttributes.append('prefix', formValues.prefix); } if (formValues.project_id) { delete cloudStorageData.project_id; specificAttributes.append('project_id', formValues.project_id); } cloudStorageData.specific_attributes = specificAttributes.toString(); if (uploadedKeyFile && !isFakeKeyFileAttached) { cloudStorageData.key_file = uploadedKeyFile; } 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, }); setUploadedKeyFile(null); }; 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', }; const credentialsBlok = (): JSX.Element => { const internalCommonProps = { ...commonProps, labelCol: { span: 8, offset: 2 }, wrapperCol: { offset: 2 }, }; if (providerType === ProviderType.AWS_S3_BUCKET && credentialsType === CredentialsType.KEY_SECRET_KEY_PAIR) { return ( <> setKeyVisibility(true)} onFocus={() => onFocusCredentialsItem('key', 'key')} onBlur={() => onBlurCredentialsItem('key', 'key', setKeyVisibility)} /> setSecretKeyVisibility(true)} onFocus={() => onFocusCredentialsItem('secretKey', 'secret_key')} onBlur={() => onBlurCredentialsItem('secretKey', 'secret_key', setSecretKeyVisibility)} /> ); } if ( providerType === ProviderType.AZURE_CONTAINER && credentialsType === CredentialsType.ACCOUNT_NAME_TOKEN_PAIR ) { return ( <> setAccountNameVisibility(true)} onFocus={() => onFocusCredentialsItem('accountName', 'account_name')} onBlur={() => onBlurCredentialsItem('accountName', 'account_name', setAccountNameVisibility)} /> setSessionTokenVisibility(true)} onFocus={() => onFocusCredentialsItem('sessionToken', 'session_token')} onBlur={() => onBlurCredentialsItem('sessionToken', 'session_token', setSessionTokenVisibility)} /> ); } if (providerType === ProviderType.AZURE_CONTAINER && credentialsType === CredentialsType.ANONYMOUS_ACCESS) { return ( <> setAccountNameVisibility(true)} /> ); } if (providerType === ProviderType.GOOGLE_CLOUD_STORAGE && credentialsType === CredentialsType.KEY_FILE_PATH) { return ( Key file )} > { setIsFakeKeyFileAttached(false); setUploadedKeyFile(file); return false; }} onRemove={() => setUploadedKeyFile(null)} > Attach a file ); } return <>; }; const AWSS3Configuration = (): JSX.Element => { const internalCommonProps = { ...commonProps, labelCol: { offset: 1 }, wrapperCol: { offset: 1 }, }; return ( <> {credentialsBlok()} ); }; const AzureBlobStorageConfiguration = (): JSX.Element => { const internalCommonProps = { ...commonProps, labelCol: { offset: 1 }, wrapperCol: { offset: 1 }, }; return ( <> {credentialsBlok()} ); }; const GoogleCloudStorageConfiguration = (): JSX.Element => { const internalCommonProps = { ...commonProps, labelCol: { span: 6, offset: 1 }, wrapperCol: { offset: 1 }, }; return ( <> {/* maxlength https://cloud.google.com/storage/docs/naming-buckets#requirements */} {credentialsBlok()} ); }; return (