diff --git a/packages/manager/cypress/e2e/core/objectStorage/access-key.e2e.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/access-key.e2e.spec.ts
index 50a79b5f9f3..9579f88f8cb 100644
--- a/packages/manager/cypress/e2e/core/objectStorage/access-key.e2e.spec.ts
+++ b/packages/manager/cypress/e2e/core/objectStorage/access-key.e2e.spec.ts
@@ -42,8 +42,8 @@ describe('object storage access key end-to-end tests', () => {
mockGetAccount(accountFactory.build({ capabilities: ['Object Storage'] }));
mockAppendFeatureFlags({
- objMultiCluster: false,
- objectStorageGen2: { enabled: false },
+ objMultiCluster: true,
+ objectStorageGen2: { enabled: true },
});
cy.visitWithLogin('/object-storage/access-keys');
@@ -59,8 +59,14 @@ describe('object storage access key end-to-end tests', () => {
.findByTitle('Create Access Key')
.should('be.visible')
.within(() => {
- cy.findByText('Label').click();
- cy.focused().type(keyLabel);
+ cy.findByLabelText('Label', { exact: false })
+ .should('be.visible')
+ .type(keyLabel);
+
+ cy.findByLabelText('Regions', { exact: false })
+ .should('be.visible')
+ .type('Atlanta, {enter}');
+
ui.buttonGroup
.findButtonByTitle('Create Access Key')
.should('be.visible')
@@ -124,6 +130,7 @@ describe('object storage access key end-to-end tests', () => {
const bucketClusterObj = chooseCluster();
const bucketRequest = createObjectStorageBucketFactoryLegacy.build({
cluster: bucketClusterObj.id,
+ cors_enabled: true,
label: bucketLabel,
// Default factory sets `cluster` and `region`, but API does not accept `region` yet.
region: undefined,
@@ -140,8 +147,8 @@ describe('object storage access key end-to-end tests', () => {
accountFactory.build({ capabilities: ['Object Storage'] })
);
mockAppendFeatureFlags({
- objMultiCluster: false,
- objectStorageGen2: { enabled: false },
+ objMultiCluster: true,
+ objectStorageGen2: { enabled: true },
});
interceptGetAccessKeys().as('getKeys');
@@ -160,10 +167,20 @@ describe('object storage access key end-to-end tests', () => {
.findByTitle('Create Access Key')
.should('be.visible')
.within(() => {
- cy.findByText('Label').click();
- cy.focused().type(keyLabel);
- cy.findByLabelText('Limited Access').click();
- cy.findByLabelText('Select read-only for all').click();
+ cy.findByLabelText('Label', { exact: false })
+ .should('be.visible')
+ .type(keyLabel);
+
+ cy.findByLabelText('Regions', { exact: false })
+ .should('be.visible')
+ .type('Atlanta, {enter}');
+
+ cy.focused().click();
+
+ cy.findByLabelText('Limited Access', { exact: false }).click();
+ cy.findByLabelText('Select read-only for all', {
+ exact: false,
+ }).click();
ui.buttonGroup
.findButtonByTitle('Create Access Key')
@@ -209,8 +226,8 @@ describe('object storage access key end-to-end tests', () => {
});
});
- const permissionLabel = `This token has read-only access for ${bucketClusterObj.id}-${bucketLabel}`;
- cy.findByLabelText(permissionLabel).should('be.visible');
+ const permissionLabel = `This access key has the following permissions:`;
+ cy.findByText(permissionLabel).should('be.visible');
});
});
});
diff --git a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts
index 7ac22a68551..e9c710f7e15 100644
--- a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts
+++ b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts
@@ -50,12 +50,19 @@ describe('object storage access keys smoke tests', () => {
.findByTitle('Create Access Key')
.should('be.visible')
.within(() => {
- cy.findByLabelText('Label').click();
- cy.focused().type(mockAccessKey.label);
+ cy.findByLabelText('Label', { exact: false })
+ .should('be.visible')
+ .type(mockAccessKey.label);
+
+ cy.findByLabelText('Regions', { exact: false })
+ .should('be.visible')
+ .type('Atlanta, {enter}');
+
ui.buttonGroup
.findButtonByTitle('Create Access Key')
.as('qaCreateAccessKey')
.scrollIntoView();
+
cy.get('@qaCreateAccessKey')
.should('be.visible')
.should('be.enabled')
diff --git a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts
index ce5a8e16584..d10d658a58a 100644
--- a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts
+++ b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts
@@ -298,10 +298,14 @@ describe('Object Storage enrollment', () => {
.findByTitle('Create Access Key')
.should('be.visible')
.within(() => {
- cy.findByLabelText('Label')
+ cy.findByLabelText('Label', { exact: false })
.should('be.visible')
.type(mockAccessKey.label);
+ cy.findByLabelText('Regions', { exact: false })
+ .should('be.visible')
+ .type('Jakarta, ID{enter}');
+
ui.buttonGroup
.findButtonByTitle('Create Access Key')
.should('be.visible')
@@ -360,7 +364,13 @@ describe('Object Storage enrollment', () => {
.findByTitle('Create Access Key')
.should('be.visible')
.within(() => {
- cy.findByLabelText('Label').should('be.visible').type(randomLabel());
+ cy.findByLabelText('Label', { exact: false })
+ .should('be.visible')
+ .type(randomLabel());
+
+ cy.findByLabelText('Regions', { exact: false })
+ .should('be.visible')
+ .type('Jakarta, ID{enter}');
ui.buttonGroup
.findButtonByTitle('Create Access Key')
diff --git a/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts
index c11d6fd890f..97c7f4507e6 100644
--- a/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts
+++ b/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts
@@ -43,7 +43,7 @@ describe('object storage smoke tests', () => {
mockAppendFeatureFlags({
gecko2: false,
objMultiCluster: true,
- objectStorageGen2: { enabled: false },
+ objectStorageGen2: { enabled: true },
}).as('getFeatureFlags');
mockGetBuckets([]).as('getBuckets');
@@ -65,7 +65,7 @@ describe('object storage smoke tests', () => {
cy.findByLabelText('Bucket Name (required)').click();
cy.focused().type(bucketLabel);
ui.regionSelect.find().click();
- cy.focused().type(`${mockCluster.id}{enter}`);
+ cy.focused().type(`${mockRegion.label}{enter}`);
ui.buttonGroup
.findButtonByTitle('Create Bucket')
.should('be.visible')
diff --git a/packages/manager/cypress/e2e/core/objectStorageMulticluster/object-storage-objects-multicluster.spec.ts b/packages/manager/cypress/e2e/core/objectStorageMulticluster/object-storage-objects-multicluster.spec.ts
index 95814459df2..a3a003e204c 100644
--- a/packages/manager/cypress/e2e/core/objectStorageMulticluster/object-storage-objects-multicluster.spec.ts
+++ b/packages/manager/cypress/e2e/core/objectStorageMulticluster/object-storage-objects-multicluster.spec.ts
@@ -27,7 +27,7 @@ const emptyFolderMessage = 'This folder is empty.';
* @returns Non-empty bucket error message.
*/
const getNonEmptyBucketMessage = (bucketLabel: string) => {
- return `Bucket ${bucketLabel} is not empty. Please delete all objects and try again.`;
+ return `The specified bucket '${bucketLabel}' is not empty. Please delete all objects before retrying.`;
};
/**
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.test.tsx
deleted file mode 100644
index 6bf0c7863fd..00000000000
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.test.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { screen } from '@testing-library/react';
-import * as React from 'react';
-
-import { objectStorageBucketFactory } from 'src/factories/objectStorage';
-import { renderWithTheme } from 'src/utilities/testHelpers';
-
-import { AccessKeyDrawer, getDefaultScopes } from './AccessKeyDrawer';
-import { getUpdatedScopes } from './AccessTable';
-
-import type { AccessKeyDrawerProps } from './AccessKeyDrawer';
-import type { MODE } from './types';
-import type { ObjectStorageKeyBucketAccess } from '@linode/api-v4/lib/object-storage/types';
-
-describe('AccessKeyDrawer', () => {
- const props: AccessKeyDrawerProps = {
- isRestrictedUser: false,
- mode: 'creating' as MODE,
- onClose: vi.fn(),
- onSubmit: vi.fn(),
- open: true,
- };
- renderWithTheme();
- it('renders without crashing', () => {
- expect(screen.getByTestId('drawer-title')).toBeInTheDocument();
- });
-
- describe('default scopes helper method', () => {
- const mockBuckets = objectStorageBucketFactory.buildList(5);
- it('should return an item for each bucket', () => {
- expect(getDefaultScopes(mockBuckets)).toHaveLength(mockBuckets.length);
- });
-
- it('should return objects with the correct shape', () => {
- const bucket = mockBuckets[0];
- expect(getDefaultScopes([bucket])[0]).toEqual({
- bucket_name: bucket.label,
- cluster: bucket.cluster,
- permissions: 'none',
- region: 'us-east',
- });
- });
-
- it('should sort the permissions by cluster', () => {
- const usaBucket = objectStorageBucketFactory.build({
- cluster: 'us-east-1',
- });
- const germanBucket = objectStorageBucketFactory.build({
- cluster: 'eu-central-1',
- });
- const asiaBucket = objectStorageBucketFactory.build({
- cluster: 'ap-south-1',
- });
- const unsortedBuckets = [usaBucket, germanBucket, asiaBucket];
- expect(
- getDefaultScopes(unsortedBuckets).map((scope) => scope.cluster)
- ).toEqual(['ap-south-1', 'eu-central-1', 'us-east-1']);
- });
- });
-
- describe('Updating scopes', () => {
- const mockBuckets = objectStorageBucketFactory.buildList(3);
-
- const mockScopes = getDefaultScopes(mockBuckets);
-
- it('should update the correct scope', () => {
- const newScope = {
- ...mockScopes[2],
- permissions: 'read_write',
- } as ObjectStorageKeyBucketAccess;
- expect(getUpdatedScopes(mockScopes, newScope)[2]).toHaveProperty(
- 'permissions',
- 'read_write'
- );
- });
-
- it('should leave other scopes unchanged', () => {
- const newScope = {
- ...mockScopes[2],
- access: 'read_write',
- } as ObjectStorageKeyBucketAccess;
- const updatedScopes = getUpdatedScopes(mockScopes, newScope);
- expect(updatedScopes[0]).toEqual(mockScopes[0]);
- expect(updatedScopes[1]).toEqual(mockScopes[1]);
- expect(updatedScopes.length).toEqual(mockScopes.length);
- });
-
- it('should handle crappy input', () => {
- const newScope = {
- bucket_name: 'not-real',
- cluster: 'totally-fake',
- permissions: 'read_only',
- region: 'us-east',
- } as ObjectStorageKeyBucketAccess;
- expect(getUpdatedScopes(mockScopes, newScope)).toEqual(mockScopes);
- });
- });
-});
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.tsx
deleted file mode 100644
index 49999cb275e..00000000000
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyDrawer.tsx
+++ /dev/null
@@ -1,294 +0,0 @@
-import { useAccountSettings } from '@linode/queries';
-import {
- ActionsPanel,
- CircleProgress,
- Drawer,
- Notice,
- TextField,
- Typography,
-} from '@linode/ui';
-import { createObjectStorageKeysSchema } from '@linode/validation/lib/objectStorageKeys.schema';
-import { Formik } from 'formik';
-import * as React from 'react';
-
-import { Link } from 'src/components/Link';
-import { useObjectStorageBuckets } from 'src/queries/object-storage/queries';
-
-import { EnableObjectStorageModal } from '../EnableObjectStorageModal';
-import { confirmObjectStorage } from '../utilities';
-import { LimitedAccessControls } from './LimitedAccessControls';
-
-import type { MODE } from './types';
-import type {
- CreateObjectStorageKeyPayload,
- ObjectStorageBucket,
- ObjectStorageKey,
- ObjectStorageKeyBucketAccess,
- ObjectStorageKeyBucketAccessPermissions,
- UpdateObjectStorageKeyPayload,
-} from '@linode/api-v4/lib/object-storage';
-import type { FormikProps } from 'formik';
-
-export interface AccessKeyDrawerProps {
- isRestrictedUser: boolean;
- mode: MODE;
- // If the mode is 'editing', we should have an ObjectStorageKey to edit
- objectStorageKey?: ObjectStorageKey;
- onClose: () => void;
- onSubmit: (
- values: CreateObjectStorageKeyPayload | UpdateObjectStorageKeyPayload,
- formikProps: FormikProps
- ) => void;
- open: boolean;
-}
-
-interface FormState {
- bucket_access: null | ObjectStorageKeyBucketAccess[];
- label: string;
-}
-
-/**
- * Helpers for converting a list of buckets
- * on the user's account into a list of
- * bucket_access in the shape the API will expect,
- * sorted by cluster.
- */
-export const sortByCluster = (
- a: ObjectStorageKeyBucketAccess,
- b: ObjectStorageKeyBucketAccess
-) => {
- if (a.cluster > b.cluster) {
- return 1;
- }
- if (a.cluster < b.cluster) {
- return -1;
- }
- return 0;
-};
-
-export const getDefaultScopes = (
- buckets: ObjectStorageBucket[]
-): ObjectStorageKeyBucketAccess[] =>
- buckets
- .map((thisBucket) => ({
- bucket_name: thisBucket.label,
- cluster: thisBucket.cluster,
- permissions: 'none' as ObjectStorageKeyBucketAccessPermissions,
- region: thisBucket.region ?? '',
- }))
- .sort(sortByCluster);
-
-export const AccessKeyDrawer = (props: AccessKeyDrawerProps) => {
- const { isRestrictedUser, mode, objectStorageKey, onClose, onSubmit, open } =
- props;
-
- const { data: accountSettings } = useAccountSettings();
-
- const {
- data: objectStorageBucketsResponse,
- error: bucketsError,
- isLoading: areBucketsLoading,
- } = useObjectStorageBuckets();
-
- const buckets = objectStorageBucketsResponse?.buckets || [];
-
- const hasBuckets = buckets?.length > 0;
-
- const hidePermissionsTable =
- bucketsError || objectStorageBucketsResponse?.buckets.length === 0;
-
- const createMode = mode === 'creating';
-
- const [dialogOpen, setDialogOpen] = React.useState(false);
- // This is for local display management only, not part of the payload
- // and so not included in Formik's types
- const [limitedAccessChecked, setLimitedAccessChecked] = React.useState(false);
-
- React.useEffect(() => {
- if (open) {
- setLimitedAccessChecked(false);
- }
- }, [open]);
-
- const title = createMode ? 'Create Access Key' : 'Edit Access Key Label';
-
- const initialLabelValue =
- !createMode && objectStorageKey ? objectStorageKey.label : '';
-
- const initialValues: FormState = {
- bucket_access: getDefaultScopes(buckets),
- label: initialLabelValue,
- };
-
- const handleSubmit = (
- values: CreateObjectStorageKeyPayload,
- formikProps: FormikProps
- ) => {
- // If the user hasn't toggled the Limited Access button,
- // don't include any bucket_access information in the payload.
-
- // If any/all values are 'none', don't include them in the response.
- let payload = {};
- if (
- mode === 'creating' &&
- values?.bucket_access !== null &&
- limitedAccessChecked
- ) {
- const access = values?.bucket_access ?? [];
- payload = {
- ...values,
- bucket_access: access.filter(
- (thisAccess) => thisAccess.permissions !== 'none'
- ),
- };
- } else {
- payload = { ...values, bucket_access: null };
- }
-
- if (mode === 'editing') {
- payload = {
- label: values.label,
- };
- }
- return onSubmit(payload, formikProps);
- };
-
- return (
-
- {areBucketsLoading ? (
-
- ) : (
-
- {(formikProps) => {
- const {
- errors,
- handleBlur,
- handleChange,
- handleSubmit,
- isSubmitting,
- setFieldValue,
- status,
- values,
- } = formikProps;
-
- const beforeSubmit = () => {
- confirmObjectStorage(
- accountSettings?.object_storage || 'active',
- formikProps,
- () => setDialogOpen(true)
- );
- };
-
- const handleScopeUpdate = (
- newScopes: ObjectStorageKeyBucketAccess[]
- ) => {
- setFieldValue('bucket_access', newScopes);
- };
-
- const handleToggleAccess = () => {
- setLimitedAccessChecked((checked) => !checked);
- // Reset scopes
- setFieldValue('bucket_access', getDefaultScopes(buckets));
- };
-
- return (
- <>
- {status && (
-
- )}
-
- {isRestrictedUser && (
-
- )}
-
- {/* Explainer copy if we're in 'creating' mode */}
- {createMode && (
-
- Generate an Access Key for use with an{' '}
-
- S3-compatible client
-
- .
-
- )}
-
- {!hasBuckets ? (
-
- This key will have unlimited access to all buckets on your
- account. The option to create a limited access key is only
- available after creating one or more buckets.
-
- ) : null}
-
-
- {createMode && !hidePermissionsTable ? (
-
- ) : null}
-
- setDialogOpen(false)}
- open={dialogOpen}
- />
- >
- );
- }}
-
- )}
-
- );
-};
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.test.tsx
index 66837df0145..8ccc8924692 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.test.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.test.tsx
@@ -5,11 +5,7 @@ import { renderWithTheme } from 'src/utilities/testHelpers';
import { AccessKeyLanding } from './AccessKeyLanding';
const props = {
- accessDrawerOpen: false,
- closeAccessDrawer: vi.fn(),
isRestrictedUser: false,
- mode: 'creating' as any,
- openAccessDrawer: vi.fn(),
};
describe('AccessKeyLanding', () => {
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx
index e624b21443f..d82964dcedf 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx
@@ -1,58 +1,34 @@
-import {
- createObjectStorageKeys,
- revokeObjectStorageKey,
- updateObjectStorageKey,
-} from '@linode/api-v4/lib/object-storage';
-import { useAccountSettings } from '@linode/queries';
import { useErrors, useOpenClose } from '@linode/utilities';
import { useNavigate } from '@tanstack/react-router';
import * as React from 'react';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
-import { SecretTokenDialog } from 'src/features/Profile/SecretTokenDialog/SecretTokenDialog';
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
-import { useObjectStorageAccessKeys } from 'src/queries/object-storage/queries';
import {
- sendCreateAccessKeyEvent,
- sendEditAccessKeyEvent,
- sendRevokeAccessKeyEvent,
-} from 'src/utilities/analytics/customEventAnalytics';
-import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils';
+ useDeleteAccessKeyMutation,
+ useObjectStorageAccessKeys,
+} from 'src/queries/object-storage/queries';
+import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
-import { useIsObjMultiClusterEnabled } from '../hooks/useIsObjectStorageGen2Enabled';
-import { AccessKeyDrawer } from './AccessKeyDrawer';
import { AccessKeyTable } from './AccessKeyTable/AccessKeyTable';
-import { OMC_AccessKeyDrawer } from './OMC_AccessKeyDrawer';
+import { useAccessKeyDrawers } from './hooks/useAccessKeyDrawers';
import { RevokeAccessKeyDialog } from './RevokeAccessKeyDialog';
-import { ViewPermissionsDrawer } from './ViewPermissionsDrawer';
-import type { MODE, OpenAccessDrawer } from './types';
import type {
CreateObjectStorageKeyPayload,
ObjectStorageKey,
- UpdateObjectStorageKeyPayload,
} from '@linode/api-v4/lib/object-storage';
-import type { FormikBag, FormikHelpers } from 'formik';
+import type { FormikBag } from 'formik';
interface Props {
- accessDrawerOpen: boolean;
- closeAccessDrawer: () => void;
isRestrictedUser: boolean;
- mode: MODE;
- openAccessDrawer: (mode: MODE) => void;
}
export type FormikProps = FormikBag;
export const AccessKeyLanding = (props: Props) => {
- const {
- accessDrawerOpen,
- closeAccessDrawer,
- isRestrictedUser,
- mode,
- openAccessDrawer,
- } = props;
+ const { isRestrictedUser } = props;
const navigate = useNavigate();
const pagination = usePaginationV2({
@@ -61,22 +37,14 @@ export const AccessKeyLanding = (props: Props) => {
preferenceKey: 'object-storage-keys-table',
});
- const { data, error, isLoading, refetch } = useObjectStorageAccessKeys({
+ const { data, error, isLoading } = useObjectStorageAccessKeys({
page: pagination.page,
page_size: pagination.pageSize,
});
+ const { mutateAsync: deleteAccessKey } = useDeleteAccessKeyMutation();
- const { data: accountSettings, refetch: requestAccountSettings } =
- useAccountSettings();
-
- // Key to display in Confirmation Modal upon creation
- const [keyToDisplay, setKeyToDisplay] =
- React.useState(null);
-
- // Key to rename (by clicking on a key's kebab menu )
- const [keyToEdit, setKeyToEdit] = React.useState(
- null
- );
+ const { drawer } = useAccessKeyDrawers();
+ const isCreateAccessKeyDrawerOpen = drawer?.type === 'create-access-key';
// Key to revoke (by clicking on a key's kebab menu )
const [keyToRevoke, setKeyToRevoke] = React.useState(
@@ -85,11 +53,8 @@ export const AccessKeyLanding = (props: Props) => {
const [isRevoking, setIsRevoking] = React.useState(false);
const [revokeErrors, setRevokeErrors] = useErrors();
- const displayKeysDialog = useOpenClose();
const revokeKeysDialog = useOpenClose();
- const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
-
// Redirect to base access keys route if current page has no data
// TODO: Remove this implementation and replace `usePagination` with `usePaginate` hook. See [M3-10442]
React.useEffect(() => {
@@ -109,123 +74,6 @@ export const AccessKeyLanding = (props: Props) => {
}
}, [data, isLoading, pagination.page, navigate]);
- const handleCreateKey = (
- values: CreateObjectStorageKeyPayload,
- {
- setErrors,
- setStatus,
- setSubmitting,
- }: FormikHelpers
- ) => {
- // Clear out status (used for general errors)
- setStatus(null);
- setSubmitting(true);
-
- createObjectStorageKeys(values)
- .then((data) => {
- setSubmitting(false);
-
- setKeyToDisplay(data);
-
- // "Refresh" keys to include the newly created key
- refetch();
-
- props.closeAccessDrawer();
- displayKeysDialog.open();
-
- // If our Redux Store says that the user doesn't have OBJ enabled,
- // it probably means they have just enabled it with the creation
- // of this key. In that case, update the Redux Store so that
- // subsequently created keys don't need to go through the
- // confirmation flow.
- if (accountSettings?.object_storage === 'disabled') {
- requestAccountSettings();
- }
-
- // @analytics
- sendCreateAccessKeyEvent();
- })
- .catch((errorResponse) => {
- // We also need to refresh account settings on failure, since, depending
- // on the error, Object Storage service might have actually been enabled.
- if (accountSettings?.object_storage === 'disabled') {
- requestAccountSettings();
- }
-
- setSubmitting(false);
-
- const errors = getAPIErrorOrDefault(
- errorResponse,
- 'There was an issue creating your Access Key.'
- );
- const mappedErrors = getErrorMap(['label'], errors);
-
- // `status` holds general errors
- if (mappedErrors.none) {
- setStatus(mappedErrors.none);
- }
-
- setErrors(mappedErrors);
- });
- };
-
- const handleEditKey = (
- values: UpdateObjectStorageKeyPayload,
- {
- setErrors,
- setStatus,
- setSubmitting,
- }: FormikHelpers
- ) => {
- // This shouldn't happen, but just in case.
- if (!keyToEdit) {
- return;
- }
-
- // Clear out status (used for general errors)
- setStatus(null);
-
- // If the new label is the same as the old one, no need to make an API
- // request. Just close the drawer and return early.
- if (values.label === keyToEdit.label) {
- return closeAccessDrawer();
- }
-
- setSubmitting(true);
-
- updateObjectStorageKey(
- keyToEdit.id,
- isObjMultiClusterEnabled ? values : { label: values.label }
- )
- .then((_) => {
- setSubmitting(false);
-
- // "Refresh" keys to display the newly updated key
- refetch();
-
- closeAccessDrawer();
-
- // @analytics
- sendEditAccessKeyEvent();
- })
- .catch((errorResponse) => {
- setSubmitting(false);
-
- const errors = getAPIErrorOrDefault(
- errorResponse,
- 'There was an issue updating your Access Key.'
- );
- const mappedErrors = getErrorMap(['label'], errors);
-
- // `status` holds general errors
- if (mappedErrors.none) {
- setStatus(mappedErrors.none);
- }
-
- setErrors(mappedErrors);
- });
- };
-
const handleRevokeKeys = () => {
// This shouldn't happen, but just in case.
if (!keyToRevoke) {
@@ -235,17 +83,11 @@ export const AccessKeyLanding = (props: Props) => {
setIsRevoking(true);
setRevokeErrors([]);
- revokeObjectStorageKey(keyToRevoke.id)
+ deleteAccessKey(keyToRevoke.id)
.then((_) => {
setIsRevoking(false);
- // "Refresh" keys to remove the newly revoked key
- refetch();
-
revokeKeysDialog.close();
-
- // @analytics
- sendRevokeAccessKeyEvent();
})
.catch((errorResponse) => {
setIsRevoking(false);
@@ -258,16 +100,6 @@ export const AccessKeyLanding = (props: Props) => {
});
};
- const openDrawer: OpenAccessDrawer = (
- mode: MODE,
- objectStorageKey: null | ObjectStorageKey = null
- ) => {
- setKeyToEdit(objectStorageKey);
- if (mode !== 'creating') {
- openAccessDrawer(mode);
- }
- };
-
const openRevokeDialog = (objectStorageKey: ObjectStorageKey) => {
setKeyToRevoke(objectStorageKey);
revokeKeysDialog.open();
@@ -281,7 +113,7 @@ export const AccessKeyLanding = (props: Props) => {
return (
{
error={error}
isLoading={isLoading}
isRestrictedUser={isRestrictedUser}
- openDrawer={openDrawer}
openRevokeDialog={openRevokeDialog}
/>
{
pageSize={pagination.pageSize}
/>
- {isObjMultiClusterEnabled ? (
-
- ) : (
-
- )}
-
-
-
void;
openRevokeDialog: (key: ObjectStorageKey) => void;
}
export const AccessKeyActionMenu = (props: Props) => {
- const {
- label,
- objectStorageKey,
- openDrawer,
- openHostnamesDrawer,
- openRevokeDialog,
- } = props;
+ const { label, objectStorageKey, openRevokeDialog } = props;
+
+ const { openDrawer } = useAccessKeyDrawers();
const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
@@ -37,20 +31,22 @@ export const AccessKeyActionMenu = (props: Props) => {
const actions = [
{
onClick: () => {
- openDrawer('editing', objectStorageKey);
+ openDrawer('edit-access-key', objectStorageKey.id);
},
title: isObjMultiClusterEnabled ? 'Edit' : 'Edit Label',
},
{
onClick: () => {
- openDrawer('viewing', objectStorageKey);
+ openDrawer('access-key-permissions', objectStorageKey.id);
},
title: 'Permissions',
},
...(isObjMultiClusterEnabled
? [
{
- onClick: openHostnamesDrawer,
+ onClick: () => {
+ openDrawer('access-key-hostnames', objectStorageKey.id);
+ },
title: 'View Regions/S3 Hostnames',
},
]
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.test.tsx
index d0f68f2e7e8..2a42a3d3ff7 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.test.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.test.tsx
@@ -12,7 +12,6 @@ describe('ObjectStorageKeyTable', () => {
error: undefined,
isLoading: false,
isRestrictedUser: false,
- openDrawer: vi.fn(),
openRevokeDialog: vi.fn(),
};
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.tsx
index 535da66a385..8adb0d64ee2 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.tsx
@@ -1,105 +1,66 @@
-import { useAccount } from '@linode/queries';
import { Hidden } from '@linode/ui';
-import { isFeatureEnabledV2 } from '@linode/utilities';
-import React, { useState } from 'react';
+import React from 'react';
import { Table } from 'src/components/Table';
import { TableBody } from 'src/components/TableBody';
import { TableCell } from 'src/components/TableCell';
import { TableHead } from 'src/components/TableHead';
import { TableRow } from 'src/components/TableRow';
-import { useFlags } from 'src/hooks/useFlags';
-import { HostNamesDrawer } from '../HostNamesDrawer';
+import { useIsObjMultiClusterEnabled } from '../../hooks/useIsObjectStorageGen2Enabled';
import { AccessKeyTableBody } from './AccessKeyTableBody';
-import type { OpenAccessDrawer } from '../types';
-import type {
- APIError,
- ObjectStorageKey,
- ObjectStorageKeyRegions,
-} from '@linode/api-v4';
+import type { APIError, ObjectStorageKey } from '@linode/api-v4';
export interface AccessKeyTableProps {
data: ObjectStorageKey[] | undefined;
error: APIError[] | null | undefined;
isLoading: boolean;
isRestrictedUser: boolean;
- openDrawer: OpenAccessDrawer;
openRevokeDialog: (objectStorageKey: ObjectStorageKey) => void;
}
export const AccessKeyTable = (props: AccessKeyTableProps) => {
- const {
- data,
- error,
- isLoading,
- isRestrictedUser,
- openDrawer,
- openRevokeDialog,
- } = props;
+ const { data, error, isLoading, isRestrictedUser, openRevokeDialog } = props;
- const [showHostNamesDrawer, setShowHostNamesDrawers] =
- useState(false);
- const [hostNames, setHostNames] = useState([]);
-
- const flags = useFlags();
- const { data: account } = useAccount();
-
- const isObjMultiClusterEnabled = isFeatureEnabledV2(
- 'Object Storage Access Key Regions',
- Boolean(flags.objMultiCluster),
- account?.capabilities ?? []
- );
+ const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
return (
- <>
-
-
-
- ({
- [theme.breakpoints.up('md')]: {
- minWidth: 120,
- },
- })}
- >
- Label
-
- Access Key
- {isObjMultiClusterEnabled && (
-
- Regions/S3 Hostnames
-
- )}
-
-
-
-
-
-
-
- {isObjMultiClusterEnabled && (
- setShowHostNamesDrawers(false)}
- open={showHostNamesDrawer}
- regions={hostNames}
+
+
+
+ ({
+ [theme.breakpoints.up('md')]: {
+ minWidth: 120,
+ },
+ })}
+ >
+ Label
+
+ Access Key
+ {isObjMultiClusterEnabled && (
+
+ Regions/S3 Hostnames
+
+ )}
+
+
+
+
+
- )}
- >
+
+
);
};
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx
index 34c0c9493a2..9e905731e64 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx
@@ -6,12 +6,7 @@ import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'
import { AccessKeyTableRow } from './AccessKeyTableRow';
-import type { OpenAccessDrawer } from '../types';
-import type {
- APIError,
- ObjectStorageKey,
- ObjectStorageKeyRegions,
-} from '@linode/api-v4';
+import type { APIError, ObjectStorageKey } from '@linode/api-v4';
interface Props {
data: ObjectStorageKey[] | undefined;
@@ -19,10 +14,7 @@ interface Props {
isLoading: boolean;
isObjMultiClusterEnabled: boolean;
isRestrictedUser: boolean;
- openDrawer: OpenAccessDrawer;
openRevokeDialog: (objectStorageKey: ObjectStorageKey) => void;
- setHostNames: (hostNames: ObjectStorageKeyRegions[]) => void;
- setShowHostNamesDrawers: (show: boolean) => void;
}
export const AccessKeyTableBody = (props: Props) => {
@@ -32,10 +24,7 @@ export const AccessKeyTableBody = (props: Props) => {
isLoading,
isObjMultiClusterEnabled,
isRestrictedUser,
- openDrawer,
openRevokeDialog,
- setHostNames,
- setShowHostNamesDrawers,
} = props;
const cols = isObjMultiClusterEnabled ? 4 : 3;
@@ -69,10 +58,7 @@ export const AccessKeyTableBody = (props: Props) => {
return data?.map((key) => (
));
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRow.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRow.tsx
index 735e987349f..19196ce9a61 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRow.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRow.tsx
@@ -14,25 +14,15 @@ import { useFlags } from 'src/hooks/useFlags';
import { AccessKeyActionMenu } from './AccessKeyActionMenu';
import { HostNameTableCell } from './HostNameTableCell';
-import type { OpenAccessDrawer } from '../types';
-import type { ObjectStorageKey, ObjectStorageKeyRegions } from '@linode/api-v4';
+import type { ObjectStorageKey } from '@linode/api-v4';
interface Props {
- openDrawer: OpenAccessDrawer;
openRevokeDialog: (storageKeyData: ObjectStorageKey) => void;
- setHostNames: (hostNames: ObjectStorageKeyRegions[]) => void;
- setShowHostNamesDrawers: (show: boolean) => void;
storageKeyData: ObjectStorageKey;
}
export const AccessKeyTableRow = (props: Props) => {
- const {
- openDrawer,
- openRevokeDialog,
- setHostNames,
- setShowHostNamesDrawers,
- storageKeyData,
- } = props;
+ const { openRevokeDialog, storageKeyData } = props;
const { data: account } = useAccount();
const flags = useFlags();
@@ -56,22 +46,13 @@ export const AccessKeyTableRow = (props: Props) => {
{isObjMultiClusterEnabled && (
-
+
)}
{
- setShowHostNamesDrawers(true);
- setHostNames(storageKeyData.regions);
- }}
openRevokeDialog={openRevokeDialog}
/>
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx
index 3bae9ecbc45..d97229407a5 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx
@@ -16,11 +16,7 @@ describe('HostNameTableCell', () => {
regions: [],
});
const { getByText } = renderWithTheme(
-
+
);
expect(getByText('None')).toBeInTheDocument();
@@ -47,11 +43,7 @@ describe('HostNameTableCell', () => {
})
);
const { findByText } = renderWithTheme(
-
+
);
const hostname = await findByText('US, Newark, NJ: alpha.test.com');
@@ -83,11 +75,7 @@ describe('HostNameTableCell', () => {
})
);
const { getByText } = renderWithTheme(
-
+
);
await waitFor(() => {
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx
index e16fa7a665b..d9239fcdc01 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx
@@ -7,16 +7,17 @@ import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip';
import { TableCell } from 'src/components/TableCell';
import { useObjectStorageRegions } from 'src/features/ObjectStorage/hooks/useObjectStorageRegions';
+import { useAccessKeyDrawers } from '../hooks/useAccessKeyDrawers';
+
import type { ObjectStorageKey, ObjectStorageKeyRegions } from '@linode/api-v4';
interface Props {
- setHostNames: (hostNames: ObjectStorageKeyRegions[]) => void;
- setShowHostNamesDrawers: (show: boolean) => void;
storageKeyData: ObjectStorageKey;
}
export const HostNameTableCell = (props: Props) => {
- const { setHostNames, setShowHostNamesDrawers, storageKeyData } = props;
+ const { storageKeyData } = props;
+ const { openDrawer } = useAccessKeyDrawers();
const { availableStorageRegions, regionsByIdMap } = useObjectStorageRegions();
@@ -50,10 +51,9 @@ export const HostNameTableCell = (props: Props) => {
<>
| + {pluralize('region', 'regions', regions.length - 1)} |
{
- setHostNames(regions);
- setShowHostNamesDrawers(true);
- }}
+ onClick={() =>
+ openDrawer('access-key-hostnames', storageKeyData.id)
+ }
>
Show All
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeysDrawerOutlet.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeysDrawerOutlet.tsx
new file mode 100644
index 00000000000..43410c88cb0
--- /dev/null
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeysDrawerOutlet.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+
+import { useObjectStorageAccessKey } from 'src/queries/object-storage/queries';
+
+import { useAccessKeyDrawers } from './hooks/useAccessKeyDrawers';
+import { HostNamesDrawer } from './HostNamesDrawer';
+import { AccessKeyDrawer } from './OMC_AccessKeyDrawer';
+import { ViewPermissionsDrawer } from './ViewPermissionsDrawer';
+
+export const AccessKeysDrawerOutlet = () => {
+ const { drawer, closeDrawer } = useAccessKeyDrawers();
+
+ const { data: objectStorageKey } = useObjectStorageAccessKey(
+ drawer?.accessKeyId
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.test.tsx
index 7ad193597b8..2f797718837 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.test.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.test.tsx
@@ -3,6 +3,7 @@ import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
+import { objectStorageKeyFactory } from 'src/factories';
import { renderWithTheme } from 'src/utilities/testHelpers';
import { HostNamesDrawer } from './HostNamesDrawer';
@@ -10,17 +11,18 @@ import { HostNamesDrawer } from './HostNamesDrawer';
// Mock the onClose function
const mockOnClose = vi.fn();
-// Mock regions data
-const mockS3Regions = [
- {
- id: 'region1',
- s3_endpoint: 'endpoint1',
- },
- {
- id: 'region2',
- s3_endpoint: 'endpoint2',
- },
-];
+const mockAccessKey = objectStorageKeyFactory.build({
+ regions: [
+ {
+ id: 'region1',
+ s3_endpoint: 'endpoint1',
+ },
+ {
+ id: 'region2',
+ s3_endpoint: 'endpoint2',
+ },
+ ],
+});
vi.mock('@linode/queries', async (importOriginal) => ({
...(await importOriginal()),
@@ -36,9 +38,9 @@ describe('HostNamesDrawer', () => {
it('renders the drawer with regions and copyable text', () => {
renderWithTheme(
);
@@ -67,9 +69,9 @@ describe('HostNamesDrawer', () => {
it('calls onClose when the drawer is closed', async () => {
renderWithTheme(
);
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.tsx
index eb9db364d80..4d63e955f7c 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.tsx
@@ -6,24 +6,27 @@ import { useObjectStorageRegions } from 'src/features/ObjectStorage/hooks/useObj
import { CopyAllHostnames } from './CopyAllHostnames';
-import type { ObjectStorageKeyRegions } from '@linode/api-v4';
+import type { ObjectStorageKey } from '@linode/api-v4';
interface Props {
+ isOpen: boolean;
+ objectStorageKey?: ObjectStorageKey;
onClose: () => void;
- open: boolean;
- regions: ObjectStorageKeyRegions[];
}
export const HostNamesDrawer = (props: Props) => {
- const { onClose, open, regions } = props;
+ const { onClose, isOpen, objectStorageKey } = props;
+
const { availableStorageRegions, regionsByIdMap } = useObjectStorageRegions();
+ const regions = objectStorageKey?.regions || [];
+
if (!availableStorageRegions || !regionsByIdMap) {
return null;
}
return (
-
+
({ marginTop: theme.spacing(3) })}>
void;
- onSubmit: (
- values: CreateObjectStorageKeyPayload | UpdateObjectStorageKeyPayload,
- formikProps: FormikProps<
- CreateObjectStorageKeyPayload | UpdateObjectStorageKeyPayload
- >
- ) => void;
- open: boolean;
}
// Access key scopes displayed in the drawer can have no permission or "No Access" selected, which are not valid API permissions.
@@ -102,9 +101,16 @@ export const getDefaultScopes = (
}))
.sort(sortByRegion(regionLookup));
-export const OMC_AccessKeyDrawer = (props: AccessKeyDrawerProps) => {
- const { isRestrictedUser, mode, objectStorageKey, onClose, onSubmit, open } =
- props;
+export const AccessKeyDrawer = (props: AccessKeyDrawerProps) => {
+ const { mode, onClose, isOpen, objectStorageKey } = props;
+
+ const { data: profile } = useProfile();
+ const isRestrictedUser = profile?.restricted ?? false;
+
+ const displayKeysDialog = useOpenClose();
+ // Key to display in Confirmation Modal upon creation
+ const [keyToDisplay, setKeyToDisplay] =
+ React.useState(null);
const { regionsByIdMap } = useObjectStorageRegions();
@@ -115,6 +121,8 @@ export const OMC_AccessKeyDrawer = (props: AccessKeyDrawerProps) => {
} = useObjectStorageBuckets();
const { data: accountSettings } = useAccountSettings();
+ const { mutateAsync: createAccessKey } = useCreateAccessKeyMutation();
+ const { mutateAsync: updateAccessKey } = useUpdateAccessKeyMutation();
const buckets = objectStorageBuckets?.buckets || [];
@@ -143,6 +151,92 @@ export const OMC_AccessKeyDrawer = (props: AccessKeyDrawerProps) => {
regions: initialRegions,
};
+ const handleCreateKey = (
+ values: CreateObjectStorageKeyPayload,
+ {
+ setErrors,
+ setStatus,
+ setSubmitting,
+ }: FormikHelpers
+ ) => {
+ // Clear out status (used for general errors)
+ setStatus(null);
+ setSubmitting(true);
+
+ createAccessKey(values)
+ .then((data) => {
+ setSubmitting(false);
+
+ setKeyToDisplay(data);
+
+ onClose();
+ displayKeysDialog.open();
+ })
+ .catch((errorResponse) => {
+ setSubmitting(false);
+
+ const errors = getAPIErrorOrDefault(
+ errorResponse,
+ 'There was an issue creating your Access Key.'
+ );
+ const mappedErrors = getErrorMap(['label'], errors);
+
+ // `status` holds general errors
+ if (mappedErrors.none) {
+ setStatus(mappedErrors.none);
+ }
+
+ setErrors(mappedErrors);
+ });
+ };
+
+ const handleEditKey = (
+ values: UpdateObjectStorageKeyPayload,
+ {
+ setErrors,
+ setStatus,
+ setSubmitting,
+ }: FormikHelpers
+ ) => {
+ // This shouldn't happen, but just in case.
+ if (!objectStorageKey) {
+ return;
+ }
+
+ // Clear out status (used for general errors)
+ setStatus(null);
+
+ // If the new label is the same as the old one, no need to make an API
+ // request. Just close the drawer and return early.
+ if (values.label === objectStorageKey.label) {
+ return onClose();
+ }
+
+ setSubmitting(true);
+
+ updateAccessKey({ data: values, id: objectStorageKey.id })
+ .then((_) => {
+ setSubmitting(false);
+ onClose();
+ })
+ .catch((errorResponse) => {
+ setSubmitting(false);
+
+ const errors = getAPIErrorOrDefault(
+ errorResponse,
+ 'There was an issue updating your Access Key.'
+ );
+ const mappedErrors = getErrorMap(['label'], errors);
+
+ // `status` holds general errors
+ if (mappedErrors.none) {
+ setStatus(mappedErrors.none);
+ }
+
+ setErrors(mappedErrors);
+ });
+ };
+
const formik = useFormik({
initialValues,
onSubmit: (values) => {
@@ -166,9 +260,9 @@ export const OMC_AccessKeyDrawer = (props: AccessKeyDrawerProps) => {
const updatePayload = generateUpdatePayload(values, initialValues);
if (mode !== 'creating') {
- onSubmit(updatePayload, formik);
+ handleEditKey(updatePayload, formik);
} else {
- onSubmit(payload, formik);
+ handleCreateKey(payload, formik);
}
},
validateOnBlur: true,
@@ -215,132 +309,141 @@ export const OMC_AccessKeyDrawer = (props: AccessKeyDrawerProps) => {
useEffect(() => {
setLimitedAccessChecked(false);
formik.resetForm({ values: initialValues });
- }, [open]);
+ }, [isOpen]);
return (
-
- {areBucketsLoading ? (
-
- ) : (
- <>
- {formik.status && (
-
+
+ {areBucketsLoading ? (
+
+ ) : (
+ <>
+ {formik.status && (
+
+ )}
+
+ {isRestrictedUser && (
+
+ )}
+
+ {/* Explainer copy if we're in 'creating' mode */}
+ {createMode && (
+
+ Generate an Access Key for use with an{' '}
+
+ S3-compatible client
+
+ .
+
+ )}
+
+ {!hasBuckets ? (
+
+ This key will have unlimited access to all buckets on your
+ account. The option to create a limited access key is only
+ available after creating one or more buckets.
+
+ ) : null}
+
+
- )}
-
- {isRestrictedUser && (
- {
+ const bucketsInRegions = buckets?.filter(
+ (bucket) => bucket.region && values.includes(bucket.region)
+ );
+ formik.setFieldValue(
+ 'bucket_access',
+ getDefaultScopes(bucketsInRegions, regionsByIdMap)
+ );
+ formik.setFieldValue('regions', values);
+ }}
+ required
+ selectedRegion={formik.values.regions}
/>
- )}
-
- {/* Explainer copy if we're in 'creating' mode */}
- {createMode && (
-
- Generate an Access Key for use with an{' '}
- ({
+ marginTop: theme.spacing(2),
+ })}
>
- S3-compatible client
-
- .
-
- )}
-
- {!hasBuckets ? (
-
- This key will have unlimited access to all buckets on your
- account. The option to create a limited access key is only
- available after creating one or more buckets.
-
- ) : null}
-
-
- {
- const bucketsInRegions = buckets?.filter(
- (bucket) => bucket.region && values.includes(bucket.region)
- );
- formik.setFieldValue(
- 'bucket_access',
- getDefaultScopes(bucketsInRegions, regionsByIdMap)
- );
- formik.setFieldValue('regions', values);
- }}
- required
- selectedRegion={formik.values.regions}
- />
- {createMode && (
- ({
- marginTop: theme.spacing(2),
- })}
- >
- Unlimited S3 access key can be used to create buckets in the
- selected region using S3 Endpoint returned on successful creation
- of the key.
-
- )}
- {createMode && !bucketsError && (
-
+ )}
+ {createMode && !bucketsError && (
+
+ )}
+
+ setDialogOpen(false)}
+ open={dialogOpen}
/>
- )}
-
- setDialogOpen(false)}
- open={dialogOpen}
- />
- >
- )}
-
+ >
+ )}
+
+
+
+ >
);
};
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/ViewPermissionsDrawer.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/ViewPermissionsDrawer.tsx
index 9bdfc6f8ebc..7d1c2ae5611 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/ViewPermissionsDrawer.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/ViewPermissionsDrawer.tsx
@@ -8,20 +8,20 @@ import { BucketPermissionsTable } from './BucketPermissionsTable';
import type { ObjectStorageKey } from '@linode/api-v4';
export interface Props {
- objectStorageKey: null | ObjectStorageKey;
+ isOpen: boolean;
+ objectStorageKey?: ObjectStorageKey;
onClose: () => void;
- open: boolean;
}
export const ViewPermissionsDrawer = (props: Props) => {
- const { objectStorageKey, onClose, open } = props;
+ const { onClose, isOpen, objectStorageKey } = props;
const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
return (
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/hooks/useAccessKeyDrawers.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/hooks/useAccessKeyDrawers.tsx
new file mode 100644
index 00000000000..246913288c3
--- /dev/null
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/hooks/useAccessKeyDrawers.tsx
@@ -0,0 +1,69 @@
+import { useMatch, useNavigate, useParams } from '@tanstack/react-router';
+import { useMemo } from 'react';
+
+type AccessKeyDrawerType =
+ | 'access-key-hostnames'
+ | 'access-key-permissions'
+ | 'create-access-key'
+ | 'edit-access-key';
+
+const ACCESS_KEYS_BASE_URL = '/object-storage/access-keys';
+
+interface AccessKeyDrawerState {
+ accessKeyId?: number;
+ type: AccessKeyDrawerType;
+}
+
+export const useAccessKeyDrawers = () => {
+ const navigate = useNavigate();
+ const { routeId } = useMatch({ strict: false });
+ const { accessKeyId } = useParams({ strict: false });
+
+ function getDrawer(): AccessKeyDrawerState | null {
+ switch (routeId) {
+ case `${ACCESS_KEYS_BASE_URL}/$accessKeyId/edit`:
+ return { accessKeyId, type: 'edit-access-key' };
+ case `${ACCESS_KEYS_BASE_URL}/$accessKeyId/hostnames`:
+ return { accessKeyId, type: 'access-key-hostnames' };
+ case `${ACCESS_KEYS_BASE_URL}/$accessKeyId/permissions`:
+ return { accessKeyId, type: 'access-key-permissions' };
+ case `${ACCESS_KEYS_BASE_URL}/create`:
+ return { type: 'create-access-key' };
+ default:
+ return null;
+ }
+ }
+
+ function openDrawer(drawer: AccessKeyDrawerType, accessKeyId?: number) {
+ switch (drawer) {
+ case 'access-key-hostnames':
+ navigate({
+ to: `${ACCESS_KEYS_BASE_URL}/${accessKeyId}/hostnames`,
+ });
+ break;
+ case 'access-key-permissions':
+ navigate({
+ to: `${ACCESS_KEYS_BASE_URL}/${accessKeyId}/permissions`,
+ });
+ break;
+ case 'create-access-key':
+ navigate({ to: `${ACCESS_KEYS_BASE_URL}/create` });
+ break;
+ case 'edit-access-key':
+ navigate({
+ to: `${ACCESS_KEYS_BASE_URL}/${accessKeyId}/edit`,
+ });
+ break;
+ }
+ }
+
+ function closeDrawer() {
+ navigate({ to: ACCESS_KEYS_BASE_URL });
+ }
+
+ return {
+ drawer: useMemo(() => getDrawer(), [routeId]),
+ openDrawer,
+ closeDrawer,
+ };
+};
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/utils.ts b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/utils.ts
index 4bad381c612..a02b335929b 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/utils.ts
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/utils.ts
@@ -1,6 +1,9 @@
import { areArraysEqual, sortByString } from '@linode/utilities';
-import type { DisplayedAccessKeyScope, FormState } from './OMC_AccessKeyDrawer';
+import type {
+ DisplayedAccessKeyScope,
+ FormState,
+} from './OMC_AccessKeyDrawer.js';
import type { ObjectStorageKey } from '@linode/api-v4/lib/object-storage';
type UpdatePayload =
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx
index 26ac6c68455..1e8e6dd54c3 100644
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx
@@ -93,9 +93,9 @@ describe('BucketDetailsDrawer: Legacy UI', () => {
renderWithThemeAndHookFormContext({
component: (
),
});
@@ -114,9 +114,9 @@ describe('BucketDetailsDrawer: Legacy UI', () => {
renderWithThemeAndHookFormContext({
component: (
),
});
@@ -128,9 +128,9 @@ describe('BucketDetailsDrawer: Legacy UI', () => {
renderWithThemeAndHookFormContext({
component: (
),
});
@@ -142,9 +142,9 @@ describe('BucketDetailsDrawer: Legacy UI', () => {
renderWithThemeAndHookFormContext({
component: (
),
});
@@ -158,9 +158,9 @@ describe('BucketDetailsDrawer: Legacy UI', () => {
renderWithThemeAndHookFormContext({
component: (
),
options: {
@@ -177,12 +177,13 @@ describe('BucketDetailsDrawer: Legacy UI', () => {
it('does not render AccessSelect when cluster or bucketLabel is missing', async () => {
const bucketWithoutCluster = { ...bucket, cluster: '' };
+
renderWithThemeAndHookFormContext({
component: (
),
options: {
@@ -209,9 +210,9 @@ describe('BucketDetailDrawer: Gen2 UI', () => {
renderWithThemeAndHookFormContext({
component: (
),
options: {
@@ -236,9 +237,9 @@ describe('BucketDetailDrawer: Gen2 UI', () => {
const { getByText } = renderWithThemeAndHookFormContext({
component: (
),
options: {
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx
index 6ffcf11205f..96de92774de 100644
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx
@@ -13,17 +13,17 @@ import { formatDate } from 'src/utilities/formatDate';
import { AccessSelect } from '../BucketDetail/AccessTab/AccessSelect';
import { useIsObjMultiClusterEnabled } from '../hooks/useIsObjectStorageGen2Enabled';
-import type { ObjectStorageBucket } from '@linode/api-v4/lib/object-storage';
+import type { ObjectStorageBucket } from '@linode/api-v4';
export interface BucketDetailsDrawerProps {
+ bucket?: ObjectStorageBucket;
+ isOpen: boolean;
onClose: () => void;
- open: boolean;
- selectedBucket: ObjectStorageBucket | undefined;
}
export const BucketDetailsDrawer = React.memo(
(props: BucketDetailsDrawerProps) => {
- const { onClose, open, selectedBucket } = props;
+ const { onClose, isOpen, bucket } = props;
const {
cluster,
@@ -34,7 +34,7 @@ export const BucketDetailsDrawer = React.memo(
objects,
region,
size,
- } = selectedBucket ?? {};
+ } = bucket ?? {};
const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
@@ -65,7 +65,7 @@ export const BucketDetailsDrawer = React.memo(
return (
{formattedCreated && (
@@ -109,7 +109,7 @@ export const BucketDetailsDrawer = React.memo(
{typeof objects === 'number' && (
{pluralize('object', 'objects', objects)}
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDrawerOutlet.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDrawerOutlet.tsx
new file mode 100644
index 00000000000..120bd6dcdd6
--- /dev/null
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDrawerOutlet.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import { useObjectStorageBucket } from 'src/queries/object-storage/queries';
+
+import { BucketDetailsDrawer } from './BucketDetailsDrawer';
+import { useBucketDrawers } from './hooks/useBucketDrawers';
+import { CreateBucketDrawer } from './OMC_CreateBucketDrawer';
+
+export const BucketDrawerOutlet = () => {
+ const { drawer, closeDrawer } = useBucketDrawers();
+
+ const { data: bucket } = useObjectStorageBucket(
+ drawer?.regionId,
+ drawer?.bucketName
+ );
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx
deleted file mode 100644
index 78667fa9c7c..00000000000
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { regionFactory } from '@linode/utilities';
-import { waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-
-import {
- accountSettingsFactory,
- objectStorageClusterFactory,
-} from 'src/factories';
-import { makeResourcePage } from 'src/mocks/serverHandlers';
-import { http, HttpResponse, server } from 'src/mocks/testServer';
-import { renderWithTheme } from 'src/utilities/testHelpers';
-
-import { CreateBucketDrawer } from './CreateBucketDrawer';
-
-const props = {
- isOpen: true,
- onClose: vi.fn(),
-};
-
-describe('CreateBucketDrawer', () => {
- it.skip('Should show a general error notice if the API returns one', async () => {
- server.use(
- http.post('*/object-storage/buckets', () => {
- return HttpResponse.json(
- { errors: [{ reason: 'Object Storage is offline!' }] },
- {
- status: 500,
- }
- );
- }),
- http.get('*/regions', async () => {
- return HttpResponse.json(
- makeResourcePage(
- regionFactory.buildList(1, { id: 'us-east', label: 'Newark, NJ' })
- )
- );
- }),
- http.get('*object-storage/clusters', () => {
- return HttpResponse.json(
- makeResourcePage(
- objectStorageClusterFactory.buildList(1, {
- id: 'us-east-1',
- region: 'us-east',
- })
- )
- );
- }),
- http.get('*/account/settings', () => {
- return HttpResponse.json(
- accountSettingsFactory.build({ object_storage: 'active' })
- );
- })
- );
-
- const { findByText, getByLabelText, getByPlaceholderText, getByTestId } =
- renderWithTheme();
-
- await userEvent.type(
- getByLabelText('Label', { exact: false }),
- 'my-test-bucket'
- );
-
- // We must waitFor because we need to load region and cluster data from the API
- await waitFor(() =>
- userEvent.selectOptions(
- getByPlaceholderText('Select a Region'),
- 'Newark, NJ (us-east-1)'
- )
- );
-
- const saveButton = getByTestId('create-bucket-button');
-
- await userEvent.click(saveButton);
-
- await findByText('Object Storage is offline!');
- });
-});
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx
deleted file mode 100644
index fc2e4d3f96a..00000000000
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-import { yupResolver } from '@hookform/resolvers/yup';
-import {
- useAccountAgreements,
- useAccountSettings,
- useMutateAccountAgreements,
- useNetworkTransferPricesQuery,
- useProfile,
- useRegionsQuery,
-} from '@linode/queries';
-import { ActionsPanel, Drawer, Notice, TextField } from '@linode/ui';
-import { CreateBucketSchema } from '@linode/validation';
-import { styled } from '@mui/material/styles';
-import * as React from 'react';
-import { Controller, useForm } from 'react-hook-form';
-
-import { EUAgreementCheckbox } from 'src/features/Account/Agreements/EUAgreementCheckbox';
-import {
- useCreateBucketMutation,
- useObjectStorageBuckets,
- useObjectStorageTypesQuery,
-} from 'src/queries/object-storage/queries';
-import { sendCreateBucketEvent } from 'src/utilities/analytics/customEventAnalytics';
-import { getGDPRDetails } from 'src/utilities/formatRegion';
-import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants';
-import { reportAgreementSigningError } from 'src/utilities/reportAgreementSigningError';
-
-import { EnableObjectStorageModal } from '../EnableObjectStorageModal';
-import { QuotasInfoNotice } from '../QuotasInfoNotice';
-import ClusterSelect from './ClusterSelect';
-import { OveragePricing } from './OveragePricing';
-
-import type { CreateObjectStorageBucketPayload } from '@linode/api-v4';
-
-interface Props {
- isOpen: boolean;
- onClose: () => void;
-}
-
-export const CreateBucketDrawer = (props: Props) => {
- const { data: profile } = useProfile();
- const { isOpen, onClose } = props;
- const isRestrictedUser = profile?.restricted;
-
- const { data: regions } = useRegionsQuery();
-
- const { data: bucketsData } = useObjectStorageBuckets();
-
- const {
- data: objTypes,
- isError: isErrorObjTypes,
- isInitialLoading: isLoadingObjTypes,
- } = useObjectStorageTypesQuery(isOpen);
- const {
- data: transferTypes,
- isError: isErrorTransferTypes,
- isInitialLoading: isLoadingTransferTypes,
- } = useNetworkTransferPricesQuery(isOpen);
-
- const isErrorTypes = isErrorTransferTypes || isErrorObjTypes;
- const isLoadingTypes = isLoadingTransferTypes || isLoadingObjTypes;
- const isInvalidPrice =
- !objTypes || !transferTypes || isErrorTypes || isErrorTransferTypes;
-
- const { isPending, mutateAsync: createBucket } = useCreateBucketMutation();
- const { data: agreements } = useAccountAgreements();
- const { mutateAsync: updateAccountAgreements } = useMutateAccountAgreements();
- const { data: accountSettings } = useAccountSettings();
- const [isEnableObjDialogOpen, setIsEnableObjDialogOpen] =
- React.useState(false);
- const [hasSignedAgreement, setHasSignedAgreement] =
- React.useState(false);
-
- const {
- control,
- formState: { errors },
- handleSubmit,
- reset,
- setError,
- watch,
- } = useForm({
- context: { buckets: bucketsData?.buckets ?? [] },
- defaultValues: {
- cluster: '',
- cors_enabled: true,
- label: '',
- },
- mode: 'onBlur',
- resolver: yupResolver(CreateBucketSchema),
- });
-
- const watchCluster = watch('cluster');
-
- const onSubmit = async (data: CreateObjectStorageBucketPayload) => {
- try {
- await createBucket(data);
-
- if (data.cluster) {
- sendCreateBucketEvent(data.cluster);
- }
-
- if (hasSignedAgreement) {
- try {
- await updateAccountAgreements({ eu_model: true });
- } catch (error) {
- reportAgreementSigningError(error);
- }
- }
-
- handleClose();
- } catch (errors) {
- for (const error of errors) {
- setError(error?.field ?? 'root', { message: error.reason });
- }
- }
- };
-
- const handleBucketFormSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- if (accountSettings?.object_storage !== 'active') {
- setIsEnableObjDialogOpen(true);
- } else {
- handleSubmit(onSubmit)();
- }
- };
-
- const clusterRegion = watchCluster
- ? regions?.find((region) => watchCluster.includes(region.id))
- : undefined;
-
- const { showGDPRCheckbox } = getGDPRDetails({
- agreements,
- profile,
- regions,
- selectedRegionId: clusterRegion?.id ?? '',
- });
-
- const handleClose = () => {
- reset();
- onClose();
- };
-
- return (
-
-
-
- );
-};
-
-const StyledEUAgreementCheckbox = styled(EUAgreementCheckbox, {
- label: 'StyledEUAgreementCheckbox',
-})(({ theme }) => ({
- marginButton: theme.spacing(3),
- marginTop: theme.spacing(3),
-}));
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx
index cc0d8c1dd48..c88bf4ed9c3 100644
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx
@@ -21,9 +21,9 @@ import {
} from 'src/utilities/analytics/customEventAnalytics';
import { CancelNotice } from '../CancelNotice';
-import { BucketDetailsDrawer } from './BucketDetailsDrawer';
import { BucketLandingEmptyState } from './BucketLandingEmptyState';
import { BucketTable } from './BucketTable';
+import { useBucketDrawers } from './hooks/useBucketDrawers';
import type { APIError, ObjectStorageBucket } from '@linode/api-v4';
import type { Theme } from '@mui/material/styles';
@@ -55,26 +55,17 @@ export const OMC_BucketLanding = (props: Props) => {
const { classes } = useStyles();
+ const { openDrawer } = useBucketDrawers();
+
const removeBucketConfirmationDialog = useOpenClose();
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState(undefined);
- const [bucketDetailDrawerOpen, setBucketDetailDrawerOpen] =
- React.useState(false);
const [selectedBucket, setSelectedBucket] = React.useState<
ObjectStorageBucket | undefined
>(undefined);
- const handleClickDetails = (bucket: ObjectStorageBucket) => {
- setBucketDetailDrawerOpen(true);
- setSelectedBucket(bucket);
- };
-
- const closeBucketDetailDrawer = () => {
- setBucketDetailDrawerOpen(false);
- };
-
const handleClickRemove = (bucket: ObjectStorageBucket) => {
setSelectedBucket(bucket);
setError(undefined);
@@ -200,7 +191,9 @@ export const OMC_BucketLanding = (props: Props) => {
+ openDrawer('bucket-details', bucket.region, bucket.label)
+ }
handleClickRemove={handleClickRemove}
handleOrderChange={handleOrderChange}
order={order}
@@ -256,11 +249,6 @@ export const OMC_BucketLanding = (props: Props) => {
Account Settings. */}
{buckets.length === 1 && }
-
);
};
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.test.tsx
index 5e3050f2f14..18d4029f84c 100644
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.test.tsx
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.test.tsx
@@ -6,14 +6,14 @@ import { makeResourcePage } from 'src/mocks/serverHandlers';
import { http, HttpResponse, server } from 'src/mocks/testServer';
import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
-import { OMC_CreateBucketDrawer } from './OMC_CreateBucketDrawer';
+import { CreateBucketDrawer } from './OMC_CreateBucketDrawer';
const props = {
isOpen: true,
onClose: vi.fn(),
};
-describe('OMC_CreateBucketDrawer', () => {
+describe('CreateBucketDrawer', () => {
beforeEach(() => {
vi.resetAllMocks();
});
@@ -21,7 +21,7 @@ describe('OMC_CreateBucketDrawer', () => {
it('should render the drawer', () => {
const { getByTestId, getByText, queryByText } =
renderWithThemeAndHookFormContext({
- component: ,
+ component: ,
options: {
flags: {
objMultiCluster: true,
@@ -56,7 +56,7 @@ describe('OMC_CreateBucketDrawer', () => {
);
const { queryByText } = renderWithThemeAndHookFormContext({
- component: ,
+ component: ,
options: {
flags: {
objMultiCluster: true,
@@ -73,7 +73,7 @@ describe('OMC_CreateBucketDrawer', () => {
it('should close the drawer', () => {
const { getByText } = renderWithThemeAndHookFormContext({
- component: ,
+ component: ,
});
const cancelButton = getByText('Cancel');
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx
index 80efcdb0223..4ce76cbb89c 100644
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx
@@ -67,7 +67,7 @@ interface EndpointOption {
s3_endpoint?: string;
}
-export const OMC_CreateBucketDrawer = (props: Props) => {
+export const CreateBucketDrawer = (props: Props) => {
const { data: profile } = useProfile();
const { isOpen, onClose } = props;
const isRestrictedUser = profile?.restricted;
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/hooks/useBucketDrawers.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/hooks/useBucketDrawers.tsx
new file mode 100644
index 00000000000..d215d363d83
--- /dev/null
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/hooks/useBucketDrawers.tsx
@@ -0,0 +1,56 @@
+import { useMatch, useNavigate, useParams } from '@tanstack/react-router';
+import { useMemo } from 'react';
+
+type BucketDrawerType = 'bucket-details' | 'create-bucket';
+
+const BUCKETS_BASE_URL = '/object-storage/buckets';
+
+interface BucketDrawerState {
+ bucketName?: string;
+ regionId?: string;
+ type: BucketDrawerType;
+}
+
+export const useBucketDrawers = () => {
+ const navigate = useNavigate();
+ const { routeId } = useMatch({ strict: false });
+ const { regionId, bucketName } = useParams({ strict: false });
+
+ function getDrawer(): BucketDrawerState | null {
+ switch (routeId) {
+ case `${BUCKETS_BASE_URL}/$regionId/$bucketName/details`:
+ return { type: 'bucket-details', regionId, bucketName };
+ case `${BUCKETS_BASE_URL}/create`:
+ return { type: 'create-bucket' };
+ default:
+ return null;
+ }
+ }
+
+ function openDrawer(
+ drawer: BucketDrawerType,
+ regionId?: string,
+ bucketName?: string
+ ) {
+ switch (drawer) {
+ case 'bucket-details':
+ navigate({
+ to: `${BUCKETS_BASE_URL}/${regionId}/${bucketName}/details`,
+ });
+ break;
+ case 'create-bucket':
+ navigate({ to: `${BUCKETS_BASE_URL}/create` });
+ break;
+ }
+ }
+
+ function closeDrawer() {
+ navigate({ to: BUCKETS_BASE_URL });
+ }
+
+ return {
+ drawer: useMemo(() => getDrawer(), [routeId]),
+ openDrawer,
+ closeDrawer,
+ };
+};
diff --git a/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx
index 00dd326b68f..7e4c0ade8cd 100644
--- a/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx
+++ b/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx
@@ -1,5 +1,4 @@
import { useAccountSettings, useProfile } from '@linode/queries';
-import { useOpenClose } from '@linode/utilities';
import { styled } from '@mui/material/styles';
import { useMatch, useNavigate } from '@tanstack/react-router';
import * as React from 'react';
@@ -13,17 +12,18 @@ import { TabPanels } from 'src/components/Tabs/TabPanels';
import { Tabs } from 'src/components/Tabs/Tabs';
import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList';
import { useFlags } from 'src/hooks/useFlags';
-import { Tab, useTabs } from 'src/hooks/useTabs';
+import { useTabs } from 'src/hooks/useTabs';
import { useObjectStorageBuckets } from 'src/queries/object-storage/queries';
import { getRestrictedResourceText } from '../Account/utils';
+import { AccessKeysDrawerOutlet } from './AccessKeyLanding/AccessKeysDrawerOutlet';
+import { useAccessKeyDrawers } from './AccessKeyLanding/hooks/useAccessKeyDrawers';
import { BillingNotice } from './BillingNotice';
-import { CreateBucketDrawer } from './BucketLanding/CreateBucketDrawer';
+import { BucketDrawerOutlet } from './BucketLanding/BucketDrawerOutlet';
+import { useBucketDrawers } from './BucketLanding/hooks/useBucketDrawers';
import { OMC_BucketLanding } from './BucketLanding/OMC_BucketLanding';
-import { OMC_CreateBucketDrawer } from './BucketLanding/OMC_CreateBucketDrawer';
-import { useIsObjMultiClusterEnabled } from './hooks/useIsObjectStorageGen2Enabled';
-import type { MODE } from './AccessKeyLanding/types';
+import type { Tab } from 'src/hooks/useTabs';
const SummaryLanding = React.lazy(() =>
import('./SummaryLanding/SummaryLanding').then((module) => ({
@@ -41,12 +41,10 @@ export const ObjectStorageLanding = () => {
const navigate = useNavigate();
const match = useMatch({ strict: false });
- const [mode, setMode] = React.useState('creating');
-
- const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
-
const { data: profile } = useProfile();
const { data: accountSettings } = useAccountSettings();
+ const { openDrawer: openBucketDrawer } = useBucketDrawers();
+ const { openDrawer: openAccessKeyDrawer } = useAccessKeyDrawers();
const isRestrictedUser = profile?.restricted ?? false;
@@ -99,26 +97,16 @@ export const ObjectStorageLanding = () => {
? 'Create Access Key'
: 'Create Bucket';
- const openDrawer = useOpenClose();
-
- const handleOpenAccessDrawer = (mode: MODE) => {
- setMode(mode);
- openDrawer.open();
- };
-
const createButtonAction = () => {
if (isAccessKeysTab) {
- navigate({ to: '/object-storage/access-keys/create' });
- handleOpenAccessDrawer('creating');
+ openAccessKeyDrawer('create-access-key');
} else {
- navigate({ to: '/object-storage/buckets/create' });
+ openBucketDrawer('create-bucket');
}
};
const isSummaryOpened = match.routeId === '/object-storage/summary';
const isCreateBucketOpen = match.routeId === '/object-storage/buckets/create';
- const isCreateAccessKeyOpen =
- match.routeId === '/object-storage/access-keys/create';
// TODO: Remove when OBJ Summary is enabled
if (match.routeId === '/object-storage/summary' && !objSummaryPage) {
@@ -126,7 +114,7 @@ export const ObjectStorageLanding = () => {
}
return (
-
+ <>
{
/>
- {
- navigate({ to: '/object-storage/access-keys' });
- openDrawer.close();
- }}
- isRestrictedUser={isRestrictedUser}
- mode={mode}
- openAccessDrawer={handleOpenAccessDrawer}
- />
+
-
- {isObjMultiClusterEnabled ? (
- navigate({ to: '/object-storage/buckets' })}
- />
- ) : (
- navigate({ to: '/object-storage/buckets' })}
- />
- )}
-
+
+
+
+ >
);
};
diff --git a/packages/manager/src/queries/object-storage/queries.ts b/packages/manager/src/queries/object-storage/queries.ts
index 7670e05273b..45e6f5719cf 100644
--- a/packages/manager/src/queries/object-storage/queries.ts
+++ b/packages/manager/src/queries/object-storage/queries.ts
@@ -1,6 +1,7 @@
import {
cancelObjectStorage,
createBucket,
+ createObjectStorageKeys,
deleteBucket,
deleteBucketWithRegion,
deleteSSLCert,
@@ -10,8 +11,10 @@ import {
getObjectStorageKeys,
getObjectURL,
getSSLCert,
+ revokeObjectStorageKey,
updateBucketAccess,
updateObjectACL,
+ updateObjectStorageKey,
uploadSSLCert,
} from '@linode/api-v4';
import {
@@ -34,6 +37,11 @@ import {
import { OBJECT_STORAGE_DELIMITER as delimiter } from 'src/constants';
import { useFlags } from 'src/hooks/useFlags';
+import {
+ sendCreateAccessKeyEvent,
+ sendEditAccessKeyEvent,
+ sendRevokeAccessKeyEvent,
+} from 'src/utilities/analytics/customEventAnalytics';
import {
getAllBucketsFromEndpoints,
@@ -50,6 +58,7 @@ import type {
APIError,
CreateObjectStorageBucketPayload,
CreateObjectStorageBucketSSLPayload,
+ CreateObjectStorageKeyPayload,
CreateObjectStorageObjectURLPayload,
ObjectStorageBucket,
ObjectStorageBucketAccess,
@@ -64,6 +73,7 @@ import type {
PriceType,
ResourcePage,
UpdateObjectStorageBucketAccessPayload,
+ UpdateObjectStorageKeyPayload,
} from '@linode/api-v4';
export const objectStorageQueries = createQueryKeys('object-storage', {
@@ -118,6 +128,109 @@ export const objectStorageQueries = createQueryKeys('object-storage', {
},
});
+/**
+ * Object Storage Access Keys
+ */
+
+export const useObjectStorageAccessKeys = (params: Params) =>
+ useQuery, APIError[]>({
+ ...objectStorageQueries.accessKeys(params),
+ placeholderData: keepPreviousData,
+ });
+
+// TODO: Optimize to use tanstack cache
+export const useObjectStorageAccessKey = (id: number | undefined) => {
+ const queryClient = useQueryClient();
+
+ if (!id) {
+ return {};
+ }
+
+ const queries = queryClient.getQueriesData({
+ queryKey: objectStorageQueries.accessKeys._def,
+ });
+
+ for (const [, data] of queries) {
+ const accessKey = (data as ResourcePage)?.data?.find(
+ (key) => key.id === id
+ );
+ if (accessKey) {
+ return { data: accessKey };
+ }
+ }
+
+ return { data: undefined };
+};
+
+export const useCreateAccessKeyMutation = () => {
+ const queryClient = useQueryClient();
+ return useMutation<
+ ObjectStorageKey,
+ APIError[],
+ CreateObjectStorageKeyPayload
+ >({
+ mutationFn: createObjectStorageKeys,
+ onSuccess() {
+ // Invalidate account settings because object storage will become enabled
+ // if a user created their first bucket.
+ queryClient.invalidateQueries({
+ queryKey: accountQueries.settings.queryKey,
+ });
+
+ // Invalidate access keys query
+ queryClient.invalidateQueries({
+ queryKey: objectStorageQueries.accessKeys._def,
+ });
+
+ // @analytics
+ sendCreateAccessKeyEvent();
+ },
+ onError() {
+ // We also need to refresh account settings on failure, since, depending
+ // on the error, Object Storage service might have actually been enabled.
+ queryClient.invalidateQueries({
+ queryKey: accountQueries.settings.queryKey,
+ });
+ },
+ });
+};
+
+export const useUpdateAccessKeyMutation = () => {
+ const queryClient = useQueryClient();
+ return useMutation<
+ ObjectStorageKey,
+ APIError[],
+ { data: UpdateObjectStorageKeyPayload; id: number }
+ >({
+ mutationFn: ({ id, data }) => updateObjectStorageKey(id, data),
+ onSuccess() {
+ // Invalidate access keys query
+ queryClient.invalidateQueries({
+ queryKey: objectStorageQueries.accessKeys._def,
+ });
+
+ // @analytics
+ sendEditAccessKeyEvent();
+ },
+ });
+};
+
+export const useDeleteAccessKeyMutation = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id) => revokeObjectStorageKey(id),
+ onSuccess() {
+ // Invalidate access keys query
+ queryClient.invalidateQueries({
+ queryKey: objectStorageQueries.accessKeys._def,
+ });
+
+ // @analytics
+ sendRevokeAccessKeyEvent();
+ },
+ });
+};
+
export const useObjectStorageEndpoints = (enabled = true) => {
const flags = useFlags();
const { data: account } = useAccount();
@@ -196,12 +309,34 @@ export const useObjectStorageBuckets = (enabled: boolean = true) => {
};
};
-export const useObjectStorageAccessKeys = (params: Params) =>
- useQuery, APIError[]>({
- ...objectStorageQueries.accessKeys(params),
- placeholderData: keepPreviousData,
+// TODO: Optimize to use tanstack cache
+export const useObjectStorageBucket = (
+ region: string | undefined,
+ label: string | undefined
+) => {
+ const queryClient = useQueryClient();
+
+ if (!region || !label) {
+ return {};
+ }
+
+ const queries = queryClient.getQueriesData({
+ queryKey: objectStorageQueries.buckets.queryKey,
});
+ for (const [, data] of queries) {
+ const bucket = (data as { buckets: ObjectStorageBucket[] })?.buckets?.find(
+ (bucket) => bucket.region === region && bucket.label === label
+ );
+
+ if (bucket) {
+ return { data: bucket };
+ }
+ }
+
+ return { data: undefined };
+};
+
export const useBucketAccess = (
clusterOrRegion: string,
bucket: string,
diff --git a/packages/manager/src/routes/objectStorage/index.ts b/packages/manager/src/routes/objectStorage/index.ts
index ad2d05b180c..a8e938f63dd 100644
--- a/packages/manager/src/routes/objectStorage/index.ts
+++ b/packages/manager/src/routes/objectStorage/index.ts
@@ -43,18 +43,27 @@ const objectStorageBucketsLandingRoute = createRoute({
)
);
-const objectStorageAccessKeysLandingRoute = createRoute({
+const objectStorageBucketCreateRoute = createRoute({
getParentRoute: () => objectStorageRoute,
- path: 'access-keys',
+ path: 'buckets/create',
}).lazy(() =>
import('src/features/ObjectStorage/objectStorageLandingLazyRoute').then(
(m) => m.objectStorageLandingLazyRoute
)
);
-const objectStorageBucketCreateRoute = createRoute({
+const objectStorageBucketDetailsRoute = createRoute({
getParentRoute: () => objectStorageRoute,
- path: 'buckets/create',
+ path: 'buckets/$regionId/$bucketName/details',
+}).lazy(() =>
+ import('src/features/ObjectStorage/objectStorageLandingLazyRoute').then(
+ (m) => m.objectStorageLandingLazyRoute
+ )
+);
+
+const objectStorageAccessKeysLandingRoute = createRoute({
+ getParentRoute: () => objectStorageRoute,
+ path: 'access-keys',
}).lazy(() =>
import('src/features/ObjectStorage/objectStorageLandingLazyRoute').then(
(m) => m.objectStorageLandingLazyRoute
@@ -70,6 +79,42 @@ const objectStorageAccessKeyCreateRoute = createRoute({
)
);
+const objectStorageAccessKeyEditRoute = createRoute({
+ getParentRoute: () => objectStorageRoute,
+ path: 'access-keys/$accessKeyId/edit',
+ parseParams: (params) => ({
+ accessKeyId: Number(params.accessKeyId),
+ }),
+}).lazy(() =>
+ import('src/features/ObjectStorage/objectStorageLandingLazyRoute').then(
+ (m) => m.objectStorageLandingLazyRoute
+ )
+);
+
+const objectStorageAccessKeyPermissionsRoute = createRoute({
+ getParentRoute: () => objectStorageRoute,
+ path: 'access-keys/$accessKeyId/permissions',
+ parseParams: (params) => ({
+ accessKeyId: Number(params.accessKeyId),
+ }),
+}).lazy(() =>
+ import('src/features/ObjectStorage/objectStorageLandingLazyRoute').then(
+ (m) => m.objectStorageLandingLazyRoute
+ )
+);
+
+const objectStorageAccessKeyHostnamesRoute = createRoute({
+ getParentRoute: () => objectStorageRoute,
+ path: 'access-keys/$accessKeyId/hostnames',
+ parseParams: (params) => ({
+ accessKeyId: Number(params.accessKeyId),
+ }),
+}).lazy(() =>
+ import('src/features/ObjectStorage/objectStorageLandingLazyRoute').then(
+ (m) => m.objectStorageLandingLazyRoute
+ )
+);
+
const objectStorageBucketDetailRoute = createRoute({
getParentRoute: () => objectStorageRoute,
path: 'buckets/$clusterId/$bucketName',
@@ -120,9 +165,13 @@ export const objectStorageRouteTree = objectStorageRoute.addChildren([
objectStorageIndexRoute.addChildren([
objectStorageSummaryLandingRoute,
objectStorageBucketsLandingRoute,
- objectStorageAccessKeysLandingRoute,
objectStorageBucketCreateRoute,
+ objectStorageBucketDetailsRoute,
+ objectStorageAccessKeysLandingRoute,
objectStorageAccessKeyCreateRoute,
+ objectStorageAccessKeyEditRoute,
+ objectStorageAccessKeyPermissionsRoute,
+ objectStorageAccessKeyHostnamesRoute,
]),
objectStorageBucketDetailRoute.addChildren([
objectStorageBucketDetailObjectsRoute,