diff --git a/src/react/locations/LocationDetails/LocationDetailsOracle.tsx b/src/react/locations/LocationDetails/LocationDetailsOracle.tsx new file mode 100644 index 000000000..0c98c632e --- /dev/null +++ b/src/react/locations/LocationDetails/LocationDetailsOracle.tsx @@ -0,0 +1,204 @@ +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 const getNamespaceAndRegion = (endpoint: string) => { + if (!endpoint) return { namespace: '', region: '' }; + const regex = + /https:\/\/(?.+)\.compat\.objectstorage\.(?.+).oraclecloud.com/; + const parts = endpoint.match(regex); + return { + namespace: parts.groups['namespace'], + region: parts.groups['region'], + }; +}; + +export default function LocationDetailsOracle({ + details, + editingExisting, + onChange, +}: LocationDetailsFormProps) { + const [formState, setFormState] = useState(() => { + return { + ...Object.assign({}, INIT_STATE, details, { + secretKey: '', + ...getNamespaceAndRegion(details.endpoint), + }), + }; + }); + 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..49f6f23f4 --- /dev/null +++ b/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx @@ -0,0 +1,75 @@ +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 namespace = 'namespace'; +const region = 'eu-paris-1'; +const targetBucketName = 'target-bucket'; +const accessKey = 'accessKey'; +const secretKey = 'secretKey'; + +describe('LocationDetailsOracle', () => { + it('should call onChange with the expected props', async () => { + //S + const props = { + details: {}, + onChange: () => {}, + locationType: ORACLE_CLOUD_LOCATION_KEY, + }; + let location = {}; + render( + //@ts-ignore + (location = l)} />, + { wrapper: Wrapper }, + ); + await waitFor(() => { + expect(selectors.namespaceSelector()).toBeInTheDocument(); + }); + //E + await userEvent.type(selectors.namespaceSelector(), namespace); + await userEvent.type(selectors.regionSelector(), region); + await userEvent.type(selectors.targetBucketSelector(), targetBucketName); + 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: targetBucketName, + accessKey: accessKey, + secretKey: secretKey, + }); + }); + it('should render the namespace and region while editing', async () => { + //S + const editProps = { + details: { + endpoint: `https://${namespace}.compat.objectstorage.${region}.oraclecloud.com`, + bucketName: targetBucketName, + accessKey: accessKey, + secretKey: secretKey, + }, + onChange: () => {}, + locationType: ORACLE_CLOUD_LOCATION_KEY, + }; + render( + //@ts-ignore + (location = l)} />, + { wrapper: Wrapper }, + ); + //V + await waitFor(() => { + expect(selectors.namespaceSelector()).toHaveValue(namespace); + }); + expect(selectors.regionSelector()).toHaveValue(region); + }); +}); 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/__tests__/LocationList.test.tsx b/src/react/locations/__tests__/LocationList.test.tsx index 27ed87e32..a10e10cd5 100644 --- a/src/react/locations/__tests__/LocationList.test.tsx +++ b/src/react/locations/__tests__/LocationList.test.tsx @@ -33,7 +33,7 @@ const server = setupServer( describe('LocationList', () => { beforeAll(() => { - jest.setTimeout(30_000); + jest.setTimeout(50_000); mockOffsetSize(500, 100); server.listen({ onUnhandledRequest: 'error' }); }); 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/react/utils/storageOptions.ts b/src/react/utils/storageOptions.ts index 81457db5c..e28cd2ebe 100644 --- a/src/react/utils/storageOptions.ts +++ b/src/react/utils/storageOptions.ts @@ -8,6 +8,7 @@ import { JAGUAR_S3_ENDPOINT, JAGUAR_S3_LOCATION_KEY, Location as LegacyLocation, + ORACLE_CLOUD_LOCATION_KEY, ORANGE_S3_ENDPOINT, ORANGE_S3_LOCATION_KEY, OUTSCALE_PUBLIC_S3_ENDPOINT, @@ -34,8 +35,8 @@ export function checkIfExternalLocation(locations: LocationInfo[]): boolean { /** * Retrieve the `LocationTypeKey` so that it can be use to to get the right * storage option. - * The `JAGUAR_S3_LOCATION_KEY` and `ORANGE_S3_LOCATION_KEY` work like - * `location-scality-ring-s3-v1` in the UI with predefine values but are not + * The `JAGUAR_S3_LOCATION_KEY`,`ORANGE_S3_LOCATION_KEY` and `ORACLE_CLOUD_LOCATION_KEY` + * work like `location-scality-ring-s3-v1` in the UI with predefine values but are not * implemented in the backend. * * We need to add extra logic because changing the backend is expensive. @@ -65,6 +66,8 @@ export const getLocationTypeKey = ( return OUTSCALE_PUBLIC_S3_LOCATION_KEY; } else if (location.details.endpoint === OUTSCALE_SNC_S3_ENDPOINT) { return OUTSCALE_SNC_S3_LOCATION_KEY; + } else if (location.details.endpoint.endsWith('oraclecloud.com')) { + return ORACLE_CLOUD_LOCATION_KEY; } else { return 'locationType' in location ? location.locationType 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'