diff --git a/src/js/mock/S3ClientMSWHandlers.ts b/src/js/mock/S3ClientMSWHandlers.ts
index 789986f0c..84e2a3ca9 100644
--- a/src/js/mock/S3ClientMSWHandlers.ts
+++ b/src/js/mock/S3ClientMSWHandlers.ts
@@ -118,6 +118,43 @@ export function mockBucketOperations(
);
}
+ if (req.url.searchParams.has('cors')) {
+ return res(
+ ctx.xml(`
+
+
+
+ `),
+ );
+ }
+
+ if (req.url.searchParams.has('acl')) {
+ return res(
+ ctx.xml(`
+
+
+
+ 1234
+ test
+
+
+
+
+ `),
+ );
+ }
+
+ if (req.url.searchParams.has('object-lock')) {
+ return res(
+ ctx.xml(`
+
+
+ Enabled
+
+ `),
+ );
+ }
+
return res(ctx.status(404));
},
);
@@ -178,3 +215,33 @@ export const mockObjectEmpty = (bucketName: string) => {
},
);
};
+
+export const mockGetBucketTagging = (bucketName: string) => {
+ return rest.get(
+ `${zenkoUITestConfig.zenkoEndpoint}/${bucketName}`,
+ (req, res, ctx) => {
+ if (req.url.searchParams.has('tagging')) {
+ return res(
+ ctx.xml(`
+
+
+
+ X-Scality-UsecaseVeeam 12
+
+ `),
+ );
+ }
+ },
+ );
+};
+
+export const mockGetBucketTaggingError = (bucketName: string) => {
+ return rest.get(
+ `${zenkoUITestConfig.zenkoEndpoint}/${bucketName}`,
+ (req, res, ctx) => {
+ if (req.url.searchParams.has('tagging')) {
+ return res(ctx.status(500));
+ }
+ },
+ );
+};
diff --git a/src/react/databrowser/buckets/details/Overview.tsx b/src/react/databrowser/buckets/details/Overview.tsx
index ba11b3c9e..14199ebf6 100644
--- a/src/react/databrowser/buckets/details/Overview.tsx
+++ b/src/react/databrowser/buckets/details/Overview.tsx
@@ -1,4 +1,10 @@
-import { ConstrainedText, Icon, Toggle, Tooltip } from '@scality/core-ui';
+import {
+ ConstrainedText,
+ Icon,
+ Toast,
+ Toggle,
+ Tooltip,
+} from '@scality/core-ui';
import { SmallerText } from '@scality/core-ui/dist/components/text/Text.component';
import { useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
@@ -10,7 +16,10 @@ import type { BucketInfo } from '../../../../types/s3';
import type { AppState } from '../../../../types/state';
import { useCurrentAccount } from '../../../DataServiceRoleProvider';
import { getBucketInfo, toggleBucketVersioning } from '../../../actions';
-import { useChangeBucketVersionning } from '../../../next-architecture/domain/business/buckets';
+import {
+ useBucketTagging,
+ useChangeBucketVersionning,
+} from '../../../next-architecture/domain/business/buckets';
import { Bucket } from '../../../next-architecture/domain/entities/bucket';
import { ButtonContainer } from '../../../ui-elements/Container';
import { DeleteBucket } from '../../../ui-elements/DeleteBucket';
@@ -26,6 +35,11 @@ import {
import { useWorkflows } from '../../../workflow/Workflows';
import { useEffect, useState } from 'react';
import { Button } from '@scality/core-ui/dist/next';
+import {
+ BUCKET_TAG_USECASE,
+ VEEAMVERSION11,
+ VEEAMVERSION12,
+} from '../../../ui-elements/Veeam/VeeamConstants';
function capitalize(string: string) {
return string.toLowerCase().replace(/^\w/, (c) => {
@@ -88,6 +102,17 @@ function Overview({ bucket, ingestionStates }: Props) {
const features = useSelector((state: AppState) => state.auth.config.features);
const { account } = useCurrentAccount();
const [isErrorModalOpen, setIsErrorModalOpen] = useState(false);
+ const [bucketTaggingToast, setBucketTaggingToast] = useState(true);
+ const { tags } = useBucketTagging({ bucketName: bucket.name });
+ const VEEAM_FEATURE_FLAG_ENABLED = features.includes('Veeam');
+ const isVeeamBucket =
+ tags.status === 'success' &&
+ (tags.value?.[BUCKET_TAG_USECASE] === VEEAMVERSION11 ||
+ tags.value?.[BUCKET_TAG_USECASE] === VEEAMVERSION12) &&
+ VEEAM_FEATURE_FLAG_ENABLED;
+
+ const isVeeam12 =
+ isVeeamBucket && tags.value?.[BUCKET_TAG_USECASE] === VEEAMVERSION12;
useEffect(() => {
dispatch(getBucketInfo(bucket.name));
@@ -136,6 +161,13 @@ function Overview({ bucket, ingestionStates }: Props) {
: null
}
/>
+ {
+ setBucketTaggingToast(false);
+ }}
+ />
@@ -208,6 +240,58 @@ function Overview({ bucket, ingestionStates }: Props) {
)}
+
+ Location
+
+ {bucketInfo.locationConstraint || 'us-east-1'}
+ {' / '}
+
+ {locations &&
+ getLocationType(locations[bucketInfo.locationConstraint])}
+
+
+
+ {features.includes(XDM_FEATURE) && (
+
+ Async Metadata updates
+
+ {ingestionValue}
+ {isIngestion && }
+
+
+ )}
+
+
+ {isVeeamBucket && (
+
+ Use-case
+
+ Use-case
+ Backup - {tags.value?.[BUCKET_TAG_USECASE]}
+
+ {isVeeam12 && (
+
+ Max repository Capacity
+
+ {/* TODO */}
+ <>5TB>
+ }
+ onClick={() => {
+ //TODO: open the modal to modify the capacity
+ }}
+ />
+
+
+ )}
+
+ )}
+
+ Data protection
+
{bucketInfo.objectLockConfiguration.ObjectLockEnabled ===
'Enabled' && (
@@ -218,12 +302,18 @@ function Overview({ bucket, ingestionStates }: Props) {
id="edit-retention-btn"
variant="outline"
label="Edit"
+ aria-label="Edit default retention"
icon={}
onClick={() => {
history.push(
`/accounts/${account?.Name}/buckets/${bucket.name}/retention-setting`,
);
}}
+ disabled={isVeeamBucket}
+ tooltip={{
+ overlay:
+ 'Edition is disabled as it is managed by Veeam.',
+ }}
/>
@@ -235,26 +325,6 @@ function Overview({ bucket, ingestionStates }: Props) {
Disabled
)}
-
- Location
-
- {bucketInfo.locationConstraint || 'us-east-1'}
- {' / '}
-
- {locations &&
- getLocationType(locations[bucketInfo.locationConstraint])}
-
-
-
- {features.includes(XDM_FEATURE) && (
-
- Async Metadata updates
-
- {ingestionValue}
- {isIngestion && }
-
-
- )}
diff --git a/src/react/databrowser/buckets/details/__tests__/Overview.test.tsx b/src/react/databrowser/buckets/details/__tests__/Overview.test.tsx
index 6642b0cc1..a60c3488a 100644
--- a/src/react/databrowser/buckets/details/__tests__/Overview.test.tsx
+++ b/src/react/databrowser/buckets/details/__tests__/Overview.test.tsx
@@ -1,4 +1,3 @@
-import * as T from '../../../../ui-elements/TableKeyValue2';
import * as actions from '../../../../actions/s3bucket';
import {
bucketInfoResponseNoVersioning,
@@ -9,17 +8,18 @@ import {
bucketInfoResponseObjectLockDefaultRetention,
} from '../../../../../js/mock/S3Client';
import Overview from '../Overview';
-import { Toggle } from '@scality/core-ui';
+import { NewWrapper, zenkoUITestConfig } from '../../../../utils/testUtil';
import {
- reduxMount,
- reduxRender,
- zenkoUITestConfig,
-} from '../../../../utils/testUtil';
-import { fireEvent, screen, waitFor, within } from '@testing-library/react';
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+ within,
+} from '@testing-library/react';
import Immutable from 'immutable';
import userEvent from '@testing-library/user-event';
import { renderWithRouterMatch } from '../../../../utils/testUtil';
-import { debug } from 'jest-preview';
+
const BUCKET = {
CreationDate: 'Tue Oct 12 2020 18:38:56',
LocationConstraint: '',
@@ -54,6 +54,7 @@ const TEST_STATE = {
counter: 0,
messages: Immutable.List(),
},
+ auth: { config: { features: ['Veeam'] } },
};
//TODO: Those tests are testing implementation details based on child component names. We should refactor them.
describe('Overview', () => {
@@ -198,6 +199,11 @@ import {
getConfigOverlay,
getStorageConsumptionMetricsHandlers,
} from '../../../../../js/mock/managementClientMSWHandlers';
+import {
+ mockBucketOperations,
+ mockGetBucketTagging,
+ mockGetBucketTaggingError,
+} from '../../../../../js/mock/S3ClientMSWHandlers';
const mockResponse =
'Enabled';
const TEST_ACCOUNT =
@@ -235,6 +241,7 @@ const server = setupServer(
zenkoUITestConfig.managementEndpoint,
INSTANCE_ID,
),
+ mockBucketOperations(),
);
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' });
@@ -242,6 +249,21 @@ beforeAll(() => {
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
+const selectors = {
+ editDefaultRetentionButton: () =>
+ screen.getByRole('button', {
+ name: /edit default retention/i,
+ }),
+ bucketTaggingErrorToastCloseButton: () =>
+ screen.getByRole('button', {
+ name: /close toast button/i,
+ }),
+ bucketTaggingErrorToast: () =>
+ screen.getByText(
+ /Encountered issues loading bucket tagging, causing uncertainty about the use-case. Please refresh the page./i,
+ ),
+};
+
describe('Overview', () => {
it('should call the updateBucketVersioning function when clicking on the toggle versioning button', async () => {
const useUpdateBucketVersioningMock = jest.fn();
@@ -273,4 +295,43 @@ describe('Overview', () => {
expect(useUpdateBucketVersioningMock).toHaveBeenCalledWith(mockResponse);
});
});
+
+ it('should display the Veeam use-case and disable the edition of default retention', async () => {
+ //Setup
+ server.use(mockGetBucketTagging(bucketName));
+ //Exersise
+ render(, {
+ wrapper: NewWrapper(),
+ });
+ //Verify
+ await waitFor(() => {
+ expect(
+ screen.getByText(new RegExp(`Backup - Veeam 12`, 'i')),
+ ).toBeInTheDocument();
+ });
+ expect(selectors.editDefaultRetentionButton()).toBeDisabled();
+ });
+
+ it('should show error toast when loading bucket tagging failed', async () => {
+ //Setup
+ server.use(mockGetBucketTaggingError(bucketName));
+ //Exersise
+ render(, {
+ wrapper: NewWrapper(),
+ });
+ //Verify
+ await waitFor(() => {
+ expect(selectors.bucketTaggingErrorToast()).toBeInTheDocument();
+ });
+ //Exersise
+ userEvent.click(selectors.bucketTaggingErrorToastCloseButton());
+ //Verify
+ await waitFor(() => {
+ expect(
+ screen.queryByText(
+ /Encountered issues loading bucket tagging, causing uncertainty about the use-case. Please refresh the page./i,
+ ),
+ ).toBe(null);
+ });
+ });
});
diff --git a/src/react/next-architecture/domain/business/buckets.test.tsx b/src/react/next-architecture/domain/business/buckets.test.tsx
index b32434230..716863e45 100644
--- a/src/react/next-architecture/domain/business/buckets.test.tsx
+++ b/src/react/next-architecture/domain/business/buckets.test.tsx
@@ -2,10 +2,15 @@ import {
DEFAULT_LOCATION,
useBucketLatestUsedCapacity,
useBucketLocationConstraint,
+ useBucketTagging,
useListBucketsForCurrentAccount,
} from './buckets';
import { IMetricsAdapter } from '../../adapters/metrics/IMetricsAdapter';
-import { RenderResult, WaitFor } from '@testing-library/react-hooks';
+import {
+ RenderResult,
+ WaitFor,
+ renderHook,
+} from '@testing-library/react-hooks';
import { MockedMetricsAdapter } from '../../adapters/metrics/MockedMetricsAdapter';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
@@ -22,11 +27,18 @@ import {
RenderAdditionalHook,
} from '../../../utils/testMultipleHooks';
import { _AuthContext } from '../../ui/AuthProvider';
-import { Wrapper, zenkoUITestConfig } from '../../../utils/testUtil';
+import {
+ NewWrapper,
+ Wrapper,
+ zenkoUITestConfig,
+} from '../../../utils/testUtil';
import {
mockBucketListing,
mockBucketOperations,
+ mockGetBucketTagging,
+ mockGetBucketTaggingError,
} from '../../../../js/mock/S3ClientMSWHandlers';
+import { BUCKET_TAG_USECASE } from '../../../ui-elements/Veeam/VeeamConstants';
jest.setTimeout(30000);
@@ -887,4 +899,46 @@ describe('Buckets domain', () => {
});
});
});
+
+ describe('useBucketTagging', () => {
+ it('should return the tags for a specific bucket', async () => {
+ //Setup
+ const BUCKET_NAME = 'bucket-name';
+ server.use(mockGetBucketTagging(BUCKET_NAME));
+ const { waitFor, result } = renderHook(
+ () => useBucketTagging({ bucketName: BUCKET_NAME }),
+ { wrapper: NewWrapper() },
+ );
+ //Exercise
+ await waitFor(() => result.current.tags.status === 'success');
+ //Verify
+ expect(result.current).toEqual({
+ tags: {
+ status: 'success',
+ value: {
+ [BUCKET_TAG_USECASE]: 'Veeam 12',
+ },
+ },
+ });
+ });
+ it('should return an error if the tags fetching failed', async () => {
+ //Setup
+ const BUCKET_NAME = 'bucket-name';
+ server.use(mockGetBucketTaggingError(BUCKET_NAME));
+ const { waitFor, result } = renderHook(
+ () => useBucketTagging({ bucketName: BUCKET_NAME }),
+ { wrapper: NewWrapper() },
+ );
+ //Exercise
+ await waitFor(() => result.current.tags.status === 'error');
+ //Verify
+ expect(result.current).toEqual({
+ tags: {
+ status: 'error',
+ title: 'An error occurred while fetching the tags',
+ reason: 'Internal Server Error',
+ },
+ });
+ });
+ });
});
diff --git a/src/react/next-architecture/domain/business/buckets.ts b/src/react/next-architecture/domain/business/buckets.ts
index 4c7b22fd0..981e179e0 100644
--- a/src/react/next-architecture/domain/business/buckets.ts
+++ b/src/react/next-architecture/domain/business/buckets.ts
@@ -17,6 +17,7 @@ import {
BucketDefaultRetentionPromiseResult,
BucketLatestUsedCapacityPromiseResult,
BucketLocationConstraintPromiseResult,
+ BucketTaggingPromiseResult,
BucketVersionningPromiseResult,
BucketsPromiseResult,
} from '../entities/bucket';
@@ -91,6 +92,12 @@ export const queries = {
enabled: !!buckets.length,
...noRefetchOptions,
}),
+ getBucketTagging: (s3Client: S3, bucketName: string) => ({
+ queryKey: ['bucketTagging', getS3ClientHash(s3Client), bucketName],
+ queryFn: () => s3Client.getBucketTagging({ Bucket: bucketName }).promise(),
+ enabled: !!bucketName && !!s3Client.config.credentials?.accessKeyId,
+ ...noRefetchOptions,
+ }),
};
/**
@@ -508,3 +515,40 @@ export const useBucketLatestUsedCapacity = ({
},
};
};
+
+export const useBucketTagging = ({
+ bucketName,
+}: {
+ bucketName: string;
+}): BucketTaggingPromiseResult => {
+ const s3Client = useS3Client();
+ const { data, status } = useQuery({
+ ...queries.getBucketTagging(s3Client, bucketName),
+ });
+
+ if (status === 'loading' || status === 'idle') {
+ return {
+ tags: {
+ status: 'loading',
+ },
+ };
+ }
+ if (status === 'error') {
+ return {
+ tags: {
+ status: 'error',
+ title: 'An error occurred while fetching the tags',
+ reason: 'Internal Server Error',
+ },
+ };
+ }
+ return {
+ tags: {
+ status: 'success',
+ value: data?.TagSet?.reduce((acc, tag) => {
+ acc[tag.Key] = tag.Value;
+ return acc;
+ }, {} as Record),
+ },
+ };
+};
diff --git a/src/react/next-architecture/domain/entities/bucket.ts b/src/react/next-architecture/domain/entities/bucket.ts
index 748f08e99..bcf1a5258 100644
--- a/src/react/next-architecture/domain/entities/bucket.ts
+++ b/src/react/next-architecture/domain/entities/bucket.ts
@@ -27,3 +27,7 @@ export type Bucket = BucketLocationConstraintPromiseResult & {
export type BucketsPromiseResult = {
buckets: PromiseResult;
};
+
+export type BucketTaggingPromiseResult = {
+ tags: PromiseResult>;
+};
diff --git a/src/react/reducers/initialConstants.ts b/src/react/reducers/initialConstants.ts
index c3c27df86..50c382995 100644
--- a/src/react/reducers/initialConstants.ts
+++ b/src/react/reducers/initialConstants.ts
@@ -36,7 +36,7 @@ export const initialAuthState: AuthState = {
stsClient: new MockSTSClient(),
managementClient: new MockManagementClient(),
config: {
- features: [],
+ features: ['Veeam'],
},
oidcLogout: null,
};
diff --git a/src/react/ui-elements/Veeam/VeeamTable.tsx b/src/react/ui-elements/Veeam/VeeamTable.tsx
index 7fac5b3eb..7bb3908ec 100644
--- a/src/react/ui-elements/Veeam/VeeamTable.tsx
+++ b/src/react/ui-elements/Veeam/VeeamTable.tsx
@@ -24,8 +24,16 @@ export default function VeeamTable(_: VeeamTableProps) {
const columns: Column<{
action: string;
+ step: number;
status?: string;
}>[] = [
+ {
+ Header: 'Step',
+ accessor: 'step',
+ Cell: ({ value }) => {
+ return {value};
+ },
+ },
{
Header: 'Action',
accessor: 'action',
diff --git a/src/react/ui-elements/Veeam/useMockData.ts b/src/react/ui-elements/Veeam/useMockData.ts
index a826581d4..638820b49 100644
--- a/src/react/ui-elements/Veeam/useMockData.ts
+++ b/src/react/ui-elements/Veeam/useMockData.ts
@@ -7,22 +7,23 @@ type MockTableData = {
const mockTableData = [
{
+ step: 1,
action: 'Create an Account',
status: undefined,
},
{
+ step: 2,
action: 'Create an User',
status: undefined,
},
+ { step: 3, action: 'Create a Bucket', status: undefined },
{
- action: 'Create a Bucket',
- status: undefined,
- },
- {
+ step: 4,
action: 'Create the Veeam policy',
status: undefined,
},
{
+ step: 5,
action: 'Attach the Veeam policy to the User',
status: undefined,
},
@@ -31,6 +32,7 @@ const mockTableData = [
status: undefined,
},
{
+ step: 7,
action: 'Enable Smart Object Storage support',
status: undefined,
},
diff --git a/src/react/utils/testUtil.tsx b/src/react/utils/testUtil.tsx
index ce02cfacd..849ede86a 100644
--- a/src/react/utils/testUtil.tsx
+++ b/src/react/utils/testUtil.tsx
@@ -7,7 +7,7 @@ import { mount, ReactWrapper } from 'enzyme';
import thunk from 'redux-thunk';
import { createMemoryHistory } from 'history';
import IAMClient from '../../js/IAMClient';
-import { QueryClient, QueryClientProvider } from 'react-query';
+import { QueryClient, QueryClientProvider, setLogger } from 'react-query';
import { Route, Router } from 'react-router-dom';
import { fireEvent, render } from '@testing-library/react';
@@ -144,12 +144,20 @@ export const queryClient = new QueryClient({
},
});
+//Note: React Query version 4 setLogger function is removed.
+setLogger({
+ log: console.log,
+ warn: console.warn,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ error: () => {},
+});
+
export const zenkoUITestConfig = {
iamEndpoint: TEST_API_BASE_URL,
managementEndpoint: TEST_API_BASE_URL,
zenkoEndpoint: TEST_API_BASE_URL,
navbarConfigUrl: TEST_API_BASE_URL,
- features: [XDM_FEATURE],
+ features: [XDM_FEATURE, 'Veeam'],
navbarEndpoint: TEST_API_BASE_URL,
stsEndpoint: TEST_API_BASE_URL,
};