diff --git a/src/react/locations/LocationDetails/LocationDetailsOracle.tsx b/src/react/locations/LocationDetails/LocationDetailsOracle.tsx new file mode 100644 index 000000000..d7a036847 --- /dev/null +++ b/src/react/locations/LocationDetails/LocationDetailsOracle.tsx @@ -0,0 +1,190 @@ +import { Input } from '@scality/core-ui/dist/components/inputv2/inputv2'; +import React, { useEffect, useState } from 'react'; +import { LocationDetailsFormProps } from '.'; +import { + FormGroup, + FormSection, +} from '@scality/core-ui/dist/components/form/Form.component'; +import { Checkbox } from '@scality/core-ui/dist/components/checkbox/Checkbox.component'; + +type State = { + bucketMatch: boolean; + accessKey: string; + secretKey: string; + bucketName: string; + namespace: string; + region: string; + endpoint: string; +}; +const INIT_STATE: State = { + bucketMatch: false, + accessKey: '', + secretKey: '', + bucketName: '', + namespace: '', + region: '', + endpoint: '', +}; + +export const oracleCloudEndpointBuilder = (namespace: string, region: string) => + `https://${namespace}.compat.objectstorage.${region}.oraclecloud.com`; + +export default function LocationDetailsOracle({ + details, + editingExisting, + onChange, +}: LocationDetailsFormProps) { + const [formState, setFormState] = useState(() => { + return { + ...Object.assign({}, INIT_STATE, details, { secretKey: '' }), + }; + }); + const onFormItemChange = (e: React.ChangeEvent) => { + const target = e.target; + const value = target.type === 'checkbox' ? target.checked : target.value; + if (target.name === 'namespace' || target.name === 'region') { + setFormState({ + ...formState, + [target.name]: value, + endpoint: oracleCloudEndpointBuilder( + target.name === 'namespace' ? (value as string) : formState.namespace, + target.name === 'region' ? (value as string) : formState.region, + ), + }); + } else { + setFormState({ ...formState, [target.name]: value }); + } + + if (onChange) { + //remove the namespace and region from the formState + //as it is not part of the LocationS3Details + const { namespace, region, ...rest } = formState; + onChange({ ...rest, [target.name]: value }); + } + }; + + //TODO check why the tests expect onChange to be called on mount + useEffect(() => { + const { namespace, region, ...rest } = formState; + onChange({ ...rest, endpoint: formState.endpoint }); + }, []); + + return ( + <> + + + } + required + labelHelpTooltip="The namespace of the object storage." + helpErrorPosition="bottom" + /> + + } + required + labelHelpTooltip="The region of the object storage." + helpErrorPosition="bottom" + /> + + } + required + label="Access Key" + helpErrorPosition="bottom" + /> + + + } + /> + + } + helpErrorPosition="bottom" + /> + + + + } + helpErrorPosition="bottom" + help="Your objects will be stored in the target bucket without a source-bucket prefix." + error={ + formState.bucketMatch + ? 'Storing multiple buckets in a location with this option enabled can lead to data loss.' + : undefined + } + /> + + + ); +} diff --git a/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx b/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx new file mode 100644 index 000000000..a27907121 --- /dev/null +++ b/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx @@ -0,0 +1,47 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Wrapper } from '../../../utils/testUtil'; +import LocationDetailsOracle from '../LocationDetailsOracle'; +import { ORACLE_CLOUD_LOCATION_KEY } from '../../../../types/config'; + +const selectors = { + namespaceSelector: () => screen.getByLabelText(/Namespace/), + regionSelector: () => screen.getByLabelText(/Region/), + targetBucketSelector: () => screen.getByLabelText(/Target Bucket Name/), + accessKeySelector: () => screen.getByLabelText(/Access Key/), + secretKeySelector: () => screen.getByLabelText(/Secret Key/), +}; +const props = { + details: {}, + onChange: () => {}, + locationType: ORACLE_CLOUD_LOCATION_KEY, +}; +const namespace = 'namespace'; +const region = 'eu-paris-1'; +describe('class ', () => { + it('should call onChange on mount', async () => { + //S + let location = {}; + render( + //@ts-ignore + (location = l)} />, + { wrapper: Wrapper }, + ); + waitFor(() => { + expect(selectors.namespaceSelector()).toBeInTheDocument(); + }); + //E + await userEvent.type(selectors.namespaceSelector(), namespace); + await userEvent.type(selectors.regionSelector(), region); + await userEvent.type(selectors.targetBucketSelector(), 'target-bucket'); + await userEvent.type(selectors.accessKeySelector(), 'accessKey'); + await userEvent.type(selectors.secretKeySelector(), 'secretKey'); + expect(location).toEqual({ + bucketMatch: false, + endpoint: `https://${namespace}.compat.objectstorage.${region}.oraclecloud.com`, + bucketName: 'target-bucket', + accessKey: 'accessKey', + secretKey: 'secretKey', + }); + }); +}); diff --git a/src/react/locations/LocationDetails/storageOptions.ts b/src/react/locations/LocationDetails/storageOptions.ts index c9a203b9f..7903ec64b 100644 --- a/src/react/locations/LocationDetails/storageOptions.ts +++ b/src/react/locations/LocationDetails/storageOptions.ts @@ -13,6 +13,7 @@ import LocationDetailsDOSpaces from './LocationDetailsDOSpaces'; import LocationDetailsGcp from './LocationDetailsGcp'; import LocationDetailsHyperdriveV2 from './LocationDetailsHyperdriveV2'; import LocationDetailsNFS from './LocationDetailsNFS'; +import LocationDetailsOracle from './LocationDetailsOracle'; import LocationDetailsSproxyd from './LocationDetailsSproxyd'; import LocationDetailsTapeDMF from './LocationDetailsTapeDMF'; import LocationDetailsWasabi from './LocationDetailsWasabi'; @@ -213,4 +214,14 @@ export const storageOptions: Record = { supportsReplicationSource: true, hasIcon: false, }, + 'location-oracle-ring-s3-v1': { + name: 'Oracle Cloud Object Storage', + short: 'Oracle', + formDetails: LocationDetailsOracle, + supportsVersioning: true, + supportsReplicationTarget: true, + supportsReplicationSource: true, + hasIcon: false, + checkCapability: 'locationTypeS3Custom', + }, }; diff --git a/src/react/locations/utils.tsx b/src/react/locations/utils.tsx index 60278e6db..48d73405d 100644 --- a/src/react/locations/utils.tsx +++ b/src/react/locations/utils.tsx @@ -3,6 +3,7 @@ import { JAGUAR_S3_LOCATION_KEY, Location as LegacyLocation, LocationTypeKey, + ORACLE_CLOUD_LOCATION_KEY, ORANGE_S3_LOCATION_KEY, OUTSCALE_PUBLIC_S3_LOCATION_KEY, OUTSCALE_SNC_S3_LOCATION_KEY, @@ -174,7 +175,8 @@ export const checkIsRingS3Reseller = (locationType: LocationTypeKey) => { locationType === JAGUAR_S3_LOCATION_KEY || locationType === ORANGE_S3_LOCATION_KEY || locationType === OUTSCALE_PUBLIC_S3_LOCATION_KEY || - locationType === OUTSCALE_SNC_S3_LOCATION_KEY + locationType === OUTSCALE_SNC_S3_LOCATION_KEY || + locationType === ORACLE_CLOUD_LOCATION_KEY ); }; diff --git a/src/types/config.ts b/src/types/config.ts index 3424ed3f8..5b556346a 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -17,6 +17,7 @@ export const OUTSCALE_SNC_S3_ENDPOINT = 'https://oos.cloudgouv-eu-west-1.outscale.com'; export const OUTSCALE_SNC_S3_LOCATION_KEY = 'location-3ds-outscale-oos-snc'; +export const ORACLE_CLOUD_LOCATION_KEY = 'location-oracle-ring-s3-v1'; export type LocationName = string; type LocationS3Type = @@ -26,7 +27,8 @@ type LocationS3Type = | 'location-orange-ring-s3-v1' | 'location-aws-s3-v1' | 'location-3ds-outscale-oos-public' - | 'location-3ds-outscale-oos-snc'; + | 'location-3ds-outscale-oos-snc' + | 'location-oracle-ring-s3-v1'; type LocationFSType = | 'location-scality-hdclient-v2' | 'location-aws-s3-v1'