diff --git a/packages/manager/src/features/Account/constants.ts b/packages/manager/src/features/Account/constants.ts
index 4b7bb56aa35..d3f361c0a63 100644
--- a/packages/manager/src/features/Account/constants.ts
+++ b/packages/manager/src/features/Account/constants.ts
@@ -5,6 +5,7 @@ export const CUSTOMER_SUPPORT = 'customer support';
export const grantTypeMap = {
account: 'Account',
bucket: 'Buckets',
+ key: 'Access Keys',
database: 'Databases',
domain: 'Domains',
firewall: 'Firewalls',
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.test.tsx
index 66837df0145..535aab60d99 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.test.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.test.tsx
@@ -4,17 +4,9 @@ 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', () => {
it('should render a table of access keys', async () => {
- const { getByTestId } = renderWithTheme();
+ const { getByTestId } = renderWithTheme();
expect(getByTestId('data-qa-access-key-table')).toBeInTheDocument();
});
});
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx
index e624b21443f..70110c9d3fa 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx
@@ -1,59 +1,22 @@
-import {
- createObjectStorageKeys,
- revokeObjectStorageKey,
- updateObjectStorageKey,
-} from '@linode/api-v4/lib/object-storage';
-import { useAccountSettings } from '@linode/queries';
+import { revokeObjectStorageKey } from '@linode/api-v4/lib/object-storage';
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';
+import { sendRevokeAccessKeyEvent } from 'src/utilities/analytics/customEventAnalytics';
+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 { 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';
-
-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;
+import type { ObjectStorageKey } from '@linode/api-v4/lib/object-storage';
+export const AccessKeyLanding = () => {
const navigate = useNavigate();
const pagination = usePaginationV2({
currentRoute: '/object-storage/access-keys',
@@ -66,18 +29,6 @@ export const AccessKeyLanding = (props: Props) => {
page_size: pagination.pageSize,
});
- 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
- );
-
// Key to revoke (by clicking on a key's kebab menu )
const [keyToRevoke, setKeyToRevoke] = React.useState(
null
@@ -85,11 +36,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 +57,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) {
@@ -260,12 +91,19 @@ export const AccessKeyLanding = (props: Props) => {
const openDrawer: OpenAccessDrawer = (
mode: MODE,
- objectStorageKey: null | ObjectStorageKey = null
+ objectStorageKey: ObjectStorageKey
) => {
- setKeyToEdit(objectStorageKey);
- if (mode !== 'creating') {
- openAccessDrawer(mode);
+ let drawerUrl = `/object-storage/access-keys/${objectStorageKey.id}`;
+
+ if (mode === 'editing') {
+ drawerUrl += `/update`;
+ }
+
+ if (mode === 'viewing') {
+ drawerUrl += `/details`;
}
+
+ navigate({ to: drawerUrl });
};
const openRevokeDialog = (objectStorageKey: ObjectStorageKey) => {
@@ -279,19 +117,18 @@ export const AccessKeyLanding = (props: Props) => {
};
return (
-
-
+ <>
+
+
+
{
pageSize={pagination.pageSize}
/>
- {isObjMultiClusterEnabled ? (
-
- ) : (
-
- )}
-
-
-
{
label={keyToRevoke?.label || ''}
numAccessKeys={data?.results || 0}
/>
-
+ >
);
};
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..69ef935404e 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.test.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.test.tsx
@@ -11,7 +11,6 @@ describe('ObjectStorageKeyTable', () => {
data: [],
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..9cfdfc1ed90 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTable.tsx
@@ -24,20 +24,12 @@ 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, openDrawer, openRevokeDialog } = props;
const [showHostNamesDrawer, setShowHostNamesDrawers] =
useState(false);
@@ -85,7 +77,6 @@ export const AccessKeyTable = (props: AccessKeyTableProps) => {
error={error}
isLoading={isLoading}
isObjMultiClusterEnabled={isObjMultiClusterEnabled}
- isRestrictedUser={isRestrictedUser}
openDrawer={openDrawer}
openRevokeDialog={openRevokeDialog}
setHostNames={setHostNames}
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx
index 34c0c9493a2..614fc07e98f 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx
@@ -18,7 +18,6 @@ interface Props {
error: APIError[] | null | undefined;
isLoading: boolean;
isObjMultiClusterEnabled: boolean;
- isRestrictedUser: boolean;
openDrawer: OpenAccessDrawer;
openRevokeDialog: (objectStorageKey: ObjectStorageKey) => void;
setHostNames: (hostNames: ObjectStorageKeyRegions[]) => void;
@@ -31,7 +30,6 @@ export const AccessKeyTableBody = (props: Props) => {
error,
isLoading,
isObjMultiClusterEnabled,
- isRestrictedUser,
openDrawer,
openRevokeDialog,
setHostNames,
@@ -40,10 +38,6 @@ export const AccessKeyTableBody = (props: Props) => {
const cols = isObjMultiClusterEnabled ? 4 : 3;
- if (isRestrictedUser) {
- return ;
- }
-
if (isLoading) {
return (
{
+describe('CreateBucketDrawerV1', () => {
it.skip('Should show a general error notice if the API returns one', async () => {
server.use(
http.post('*/object-storage/buckets', () => {
@@ -54,7 +54,7 @@ describe('CreateBucketDrawer', () => {
);
const { findByText, getByLabelText, getByPlaceholderText, getByTestId } =
- renderWithTheme();
+ renderWithTheme();
await userEvent.type(
getByLabelText('Label', { exact: false }),
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx
index fc2e4d3f96a..7e57286982d 100644
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx
@@ -25,7 +25,7 @@ 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 { QuotasInfoNotice } from '../ObjectStorageBanners/QuotasInfoNotice';
import ClusterSelect from './ClusterSelect';
import { OveragePricing } from './OveragePricing';
@@ -36,7 +36,7 @@ interface Props {
onClose: () => void;
}
-export const CreateBucketDrawer = (props: Props) => {
+export const CreateBucketDrawerV1 = (props: Props) => {
const { data: profile } = useProfile();
const { isOpen, onClose } = props;
const isRestrictedUser = profile?.restricted;
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx
index cc0d8c1dd48..f112467f97a 100644
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx
@@ -1,4 +1,3 @@
-import { useProfile } from '@linode/queries';
import { CircleProgress, ErrorState, Notice, Typography } from '@linode/ui';
import { readableBytes, useOpenClose } from '@linode/utilities';
import Grid from '@mui/material/Grid';
@@ -20,31 +19,22 @@ import {
sendDeleteBucketFailedEvent,
} from 'src/utilities/analytics/customEventAnalytics';
-import { CancelNotice } from '../CancelNotice';
+import { CancelNotice } from '../ObjectStorageBanners/CancelNotice';
import { BucketDetailsDrawer } from './BucketDetailsDrawer';
-import { BucketLandingEmptyState } from './BucketLandingEmptyState';
import { BucketTable } from './BucketTable';
import type { APIError, ObjectStorageBucket } from '@linode/api-v4';
import type { Theme } from '@mui/material/styles';
-interface Props {
- isCreateBucketDrawerOpen?: boolean;
-}
-
const useStyles = makeStyles()((theme: Theme) => ({
copy: {
marginTop: theme.spacing(),
},
}));
-export const OMC_BucketLanding = (props: Props) => {
- const { isCreateBucketDrawerOpen } = props;
- const { data: profile } = useProfile();
+export const OMC_BucketLanding = () => {
const { availableStorageRegions } = useObjectStorageRegions();
- const isRestrictedUser = profile?.restricted;
-
const {
data: objectStorageBucketsResponse,
error: bucketsErrors,
@@ -161,10 +151,6 @@ export const OMC_BucketLanding = (props: Props) => {
preferenceKey: 'object-storage-buckets',
});
- if (isRestrictedUser) {
- return ;
- }
-
if (bucketsErrors) {
return (
{
if (objectStorageBucketsResponse?.buckets.length === 0) {
return (
- <>
- {unavailableRegionLabels && unavailableRegionLabels.length > 0 && (
-
- )}
-
- >
+ unavailableRegionLabels &&
+ unavailableRegionLabels.length > 0 && (
+
+ )
);
}
return (
-
-
+ <>
+
+
{unavailableRegionLabels && unavailableRegionLabels.length > 0 && (
)}
+
{
order={order}
orderBy={orderBy}
/>
+
{/* If there's more than one Bucket, display the total usage. */}
{buckets.length > 1 ? (
{
) : null}
1 ? 8 : 18} />
+
{
Account Settings. */}
{buckets.length === 1 && }
+
-
+ >
);
};
-const RenderEmpty = () => {
- return ;
-};
-
interface UnavailableRegionLabelsProps {
regionLabels: string[];
}
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx
index 80efcdb0223..e391e1e7e87 100644
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx
+++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx
@@ -32,7 +32,7 @@ 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 { QuotasInfoNotice } from '../ObjectStorageBanners/QuotasInfoNotice';
import { BucketRegions } from './BucketRegions';
import { StyledEUAgreementCheckbox } from './OMC_CreateBucketDrawer.styles';
import { OveragePricing } from './OveragePricing';
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLandingEmptyState.tsx b/packages/manager/src/features/ObjectStorage/BucketsLandingPage.tsx
similarity index 59%
rename from packages/manager/src/features/ObjectStorage/BucketLanding/BucketLandingEmptyState.tsx
rename to packages/manager/src/features/ObjectStorage/BucketsLandingPage.tsx
index a7f5fb05001..b834fa3327e 100644
--- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLandingEmptyState.tsx
+++ b/packages/manager/src/features/ObjectStorage/BucketsLandingPage.tsx
@@ -1,4 +1,3 @@
-import { useProfile } from '@linode/queries';
import { useNavigate } from '@tanstack/react-router';
import * as React from 'react';
@@ -6,25 +5,27 @@ import { ResourcesSection } from 'src/components/EmptyLandingPageResources/Resou
import { getRestrictedResourceText } from 'src/features/Account/utils';
import { sendEvent } from 'src/utilities/analytics/utils';
+import { StyledBucketIcon } from './BucketLanding/StylesBucketIcon';
import {
gettingStartedGuides,
headers,
linkAnalyticsEvent,
youtubeLinkData,
-} from './BucketLandingEmptyResourcesData';
-import { StyledBucketIcon } from './StylesBucketIcon';
+} from './BucketsLandingPageResourcesData';
-export const BucketLandingEmptyState = () => {
+interface Props {
+ isRestrictedUser: boolean;
+}
+
+export const BucketsLandingPage = ({ isRestrictedUser }: Props) => {
const navigate = useNavigate();
- const { data: profile } = useProfile();
- const isBucketCreationRestricted = profile?.restricted;
return (
{
sendEvent({
action: 'Click:button',
@@ -39,6 +40,23 @@ export const BucketLandingEmptyState = () => {
resourceType: 'Buckets',
}),
},
+ {
+ children: 'Create Access Key',
+ disabled: isRestrictedUser,
+ onClick: () => {
+ sendEvent({
+ action: 'Click:button',
+ category: linkAnalyticsEvent.category,
+ label: 'Create Access Key',
+ });
+ navigate({ to: '/object-storage/access-keys/create' });
+ },
+ tooltipText: getRestrictedResourceText({
+ action: 'create',
+ isSingular: false,
+ resourceType: 'Access Keys',
+ }),
+ },
]}
gettingStartedGuidesData={gettingStartedGuides}
headers={headers}
diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLandingEmptyResourcesData.ts b/packages/manager/src/features/ObjectStorage/BucketsLandingPageResourcesData.ts
similarity index 100%
rename from packages/manager/src/features/ObjectStorage/BucketLanding/BucketLandingEmptyResourcesData.ts
rename to packages/manager/src/features/ObjectStorage/BucketsLandingPageResourcesData.ts
diff --git a/packages/manager/src/features/ObjectStorage/BillingNotice.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageBanners/BillingNotice.tsx
similarity index 100%
rename from packages/manager/src/features/ObjectStorage/BillingNotice.tsx
rename to packages/manager/src/features/ObjectStorage/ObjectStorageBanners/BillingNotice.tsx
diff --git a/packages/manager/src/features/ObjectStorage/CancelNotice.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageBanners/CancelNotice.tsx
similarity index 100%
rename from packages/manager/src/features/ObjectStorage/CancelNotice.tsx
rename to packages/manager/src/features/ObjectStorage/ObjectStorageBanners/CancelNotice.tsx
diff --git a/packages/manager/src/features/ObjectStorage/QuotasInfoNotice.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageBanners/QuotasInfoNotice.tsx
similarity index 100%
rename from packages/manager/src/features/ObjectStorage/QuotasInfoNotice.tsx
rename to packages/manager/src/features/ObjectStorage/ObjectStorageBanners/QuotasInfoNotice.tsx
diff --git a/packages/manager/src/features/ObjectStorage/ObjectStorageBanners/StyledPromotionalOfferCard.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageBanners/StyledPromotionalOfferCard.tsx
new file mode 100644
index 00000000000..efb55f79069
--- /dev/null
+++ b/packages/manager/src/features/ObjectStorage/ObjectStorageBanners/StyledPromotionalOfferCard.tsx
@@ -0,0 +1,9 @@
+import { styled } from '@mui/material/styles';
+
+import { PromotionalOfferCard } from 'src/components/PromotionalOfferCard/PromotionalOfferCard';
+
+export const StyledPromotionalOfferCard = styled(PromotionalOfferCard, {
+ label: 'StyledPromotionalOfferCard',
+})(({ theme }) => ({
+ marginBottom: theme.spacingFunction(0.5),
+}));
diff --git a/packages/manager/src/features/ObjectStorage/ObjectStorageDrawers/AccessKeyDrawers.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageDrawers/AccessKeyDrawers.tsx
new file mode 100644
index 00000000000..a49c9cc90b3
--- /dev/null
+++ b/packages/manager/src/features/ObjectStorage/ObjectStorageDrawers/AccessKeyDrawers.tsx
@@ -0,0 +1,196 @@
+import { useProfile } from '@linode/queries';
+import { useOpenClose } from '@linode/utilities';
+import { useMatch, useNavigate, useParams } from '@tanstack/react-router';
+import * as React from 'react';
+
+import { SecretTokenDialog } from 'src/features/Profile/SecretTokenDialog/SecretTokenDialog';
+import {
+ useCreateAccessKeyMutation,
+ useObjectStorageAccessKey,
+ useUpdateAccessKeyMutation,
+} from 'src/queries/object-storage/queries';
+import {
+ sendCreateAccessKeyEvent,
+ sendEditAccessKeyEvent,
+} from 'src/utilities/analytics/customEventAnalytics';
+import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils';
+
+import { AccessKeyDrawer } from '../AccessKeyLanding/AccessKeyDrawer';
+import { OMC_AccessKeyDrawer } from '../AccessKeyLanding/OMC_AccessKeyDrawer';
+import { useIsObjMultiClusterEnabled } from '../hooks/useIsObjectStorageGen2Enabled';
+import { AccessKeyPermissionsDrawer } from './AccessKeyPermissionsDrawer';
+
+import type {
+ CreateObjectStorageKeyPayload,
+ ObjectStorageKey,
+ UpdateObjectStorageKeyPayload,
+} from '@linode/api-v4/lib/object-storage';
+import type { FormikHelpers } from 'formik';
+
+export const AccessKeyDrawers = () => {
+ const navigate = useNavigate();
+ const { routeId } = useMatch({ strict: false });
+ const { accessKeyId } = useParams({ strict: false });
+
+ const { data: profile } = useProfile();
+ const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
+
+ const isRestrictedUser = !!profile?.restricted;
+ const isCreating = routeId.endsWith('/access-keys/create');
+ const mode = isCreating ? 'creating' : 'editing';
+
+ // TODO: Move into the drawer component itself
+ const isCreateEditOpened =
+ isCreating || routeId.endsWith(`$accessKeyId/update`);
+
+ const isPermissionsOpened = routeId.endsWith(`$accessKeyId/details`);
+
+ const displayKeysDialog = useOpenClose();
+
+ // Key to display in Confirmation Modal upon creation
+ const [keyToDisplay, setKeyToDisplay] =
+ React.useState(null);
+
+ const { data: accessKey } = useObjectStorageAccessKey(accessKeyId || -1);
+ const { mutateAsync: createAccessKey } = useCreateAccessKeyMutation();
+ const { mutateAsync: updateAccessKey } = useUpdateAccessKeyMutation();
+
+ const onClose = () => navigate({ to: '/object-storage/access-keys' });
+
+ 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();
+
+ // @analytics
+ sendCreateAccessKeyEvent();
+ })
+ .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 (!accessKey) {
+ onClose();
+ 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 === accessKey.label) {
+ onClose();
+ return;
+ }
+
+ setSubmitting(true);
+
+ updateAccessKey({
+ id: accessKey.id,
+ data: isObjMultiClusterEnabled ? values : { label: values.label },
+ })
+ .then(() => {
+ setSubmitting(false);
+
+ onClose();
+
+ // @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);
+ });
+ };
+
+ return (
+ <>
+ {isObjMultiClusterEnabled ? (
+
+ ) : (
+
+ )}
+
+ {accessKey && (
+
+ )}
+
+ {/* TODO: Move into the drawer component itself */}
+
+ >
+ );
+};
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/ViewPermissionsDrawer.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageDrawers/AccessKeyPermissionsDrawer.tsx
similarity index 59%
rename from packages/manager/src/features/ObjectStorage/AccessKeyLanding/ViewPermissionsDrawer.tsx
rename to packages/manager/src/features/ObjectStorage/ObjectStorageDrawers/AccessKeyPermissionsDrawer.tsx
index 9bdfc6f8ebc..a1e82b02257 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/ViewPermissionsDrawer.tsx
+++ b/packages/manager/src/features/ObjectStorage/ObjectStorageDrawers/AccessKeyPermissionsDrawer.tsx
@@ -1,35 +1,37 @@
import { Drawer, Typography } from '@linode/ui';
import * as React from 'react';
+import { AccessTable } from '../AccessKeyLanding/AccessTable';
+import { BucketPermissionsTable } from '../AccessKeyLanding/BucketPermissionsTable';
import { useIsObjMultiClusterEnabled } from '../hooks/useIsObjectStorageGen2Enabled';
-import { AccessTable } from './AccessTable';
-import { BucketPermissionsTable } from './BucketPermissionsTable';
import type { ObjectStorageKey } from '@linode/api-v4';
export interface Props {
- objectStorageKey: null | ObjectStorageKey;
+ isOpened: boolean;
+ objcetStorageKey: ObjectStorageKey;
onClose: () => void;
- open: boolean;
}
-export const ViewPermissionsDrawer = (props: Props) => {
- const { objectStorageKey, onClose, open } = props;
-
+export const AccessKeyPermissionsDrawer = ({
+ onClose,
+ objcetStorageKey,
+ isOpened,
+}: Props) => {
const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
return (
- {!objectStorageKey ? null : objectStorageKey.limited === false ? (
+ {!objcetStorageKey ? null : objcetStorageKey.limited === false ? (
This key has unlimited access to all buckets on your account.
- ) : objectStorageKey.bucket_access === null ? (
+ ) : objcetStorageKey.bucket_access === null ? (
This key has no permissions.
) : (
<>
@@ -39,15 +41,15 @@ export const ViewPermissionsDrawer = (props: Props) => {
{isObjMultiClusterEnabled ? (
null}
/>
) : (
null}
/>
diff --git a/packages/manager/src/features/ObjectStorage/ObjectStorageDrawers/CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageDrawers/CreateBucketDrawer.tsx
new file mode 100644
index 00000000000..61ca04b9710
--- /dev/null
+++ b/packages/manager/src/features/ObjectStorage/ObjectStorageDrawers/CreateBucketDrawer.tsx
@@ -0,0 +1,22 @@
+import { useMatch, useNavigate } from '@tanstack/react-router';
+import React from 'react';
+
+import { CreateBucketDrawerV1 } from '../BucketLanding/CreateBucketDrawer';
+import { OMC_CreateBucketDrawer } from '../BucketLanding/OMC_CreateBucketDrawer';
+import { useIsObjMultiClusterEnabled } from '../hooks/useIsObjectStorageGen2Enabled';
+
+export const CreateBucketDrawer = () => {
+ const navigate = useNavigate();
+ const { routeId } = useMatch({ strict: false });
+
+ const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
+ const isOpen = routeId.endsWith('/buckets/create');
+
+ const onClose = () => navigate({ to: '/object-storage/buckets' });
+
+ return isObjMultiClusterEnabled ? (
+
+ ) : (
+
+ );
+};
diff --git a/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx
index 00dd326b68f..74a13091ab6 100644
--- a/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx
+++ b/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx
@@ -1,216 +1,100 @@
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';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { LandingHeader } from 'src/components/LandingHeader';
-import { PromotionalOfferCard } from 'src/components/PromotionalOfferCard/PromotionalOfferCard';
-import { SuspenseLoader } from 'src/components/SuspenseLoader';
-import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel';
-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 { useObjectStorageBuckets } from 'src/queries/object-storage/queries';
import { getRestrictedResourceText } from '../Account/utils';
-import { BillingNotice } from './BillingNotice';
-import { CreateBucketDrawer } from './BucketLanding/CreateBucketDrawer';
-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';
-
-const SummaryLanding = React.lazy(() =>
- import('./SummaryLanding/SummaryLanding').then((module) => ({
- default: module.SummaryLanding,
- }))
-);
-const AccessKeyLanding = React.lazy(() =>
- import('./AccessKeyLanding/AccessKeyLanding').then((module) => ({
- default: module.AccessKeyLanding,
- }))
-);
+import { BucketsLandingPage } from './BucketsLandingPage';
+import { AccessKeyDrawers } from './ObjectStorageDrawers/AccessKeyDrawers';
+import { CreateBucketDrawer } from './ObjectStorageDrawers/CreateBucketDrawer';
+import { ObjectStorageTabs } from './ObjectStorageTabs';
export const ObjectStorageLanding = () => {
- const { promotionalOffers, objSummaryPage } = useFlags();
const navigate = useNavigate();
- const match = useMatch({ strict: false });
+ const { routeId } = useMatch({ strict: false });
- const [mode, setMode] = React.useState('creating');
-
- const { isObjMultiClusterEnabled } = useIsObjMultiClusterEnabled();
+ const { objSummaryPage } = useFlags();
const { data: profile } = useProfile();
const { data: accountSettings } = useAccountSettings();
const isRestrictedUser = profile?.restricted ?? false;
+ const isObjectStorageEnabled = accountSettings?.object_storage === 'active';
+ const isLandingPageShown = !isObjectStorageEnabled || isRestrictedUser;
- const {
- data: objectStorageBucketsResponse,
- error: bucketsErrors,
- isLoading: areBucketsLoading,
- } = useObjectStorageBuckets();
-
- const userHasNoBucketCreated =
- objectStorageBucketsResponse?.buckets.length === 0;
-
- // TODO: Remove when OBJ Summary is enabled
- const objTabs: Tab[] = [
- { title: 'Buckets', to: '/object-storage/buckets' },
- { title: 'Access Keys', to: '/object-storage/access-keys' },
- ];
-
- if (objSummaryPage) {
- objTabs.unshift({ title: 'Summary', to: '/object-storage/summary' });
- }
-
- const { handleTabChange, tabIndex, tabs, getTabIndex } = useTabs(objTabs);
-
- const summaryTabIndex = getTabIndex('/object-storage/summary');
- const bucketsTabIndex = getTabIndex('/object-storage/buckets');
- const accessKeysTabIndex = getTabIndex('/object-storage/access-keys');
-
- const objPromotionalOffers =
- promotionalOffers?.filter((offer) =>
- offer.features.includes('Object Storage')
- ) ?? [];
+ const isSummaryOpened = routeId === '/object-storage/summary';
+ const isAccessKeysOpened = routeId === '/object-storage/access-keys';
- // Users must explicitly cancel Object Storage in their Account Settings to avoid being billed.
- // Display a warning if the service is active but no buckets are present.
- const shouldDisplayBillingNotice =
- !areBucketsLoading &&
- !bucketsErrors &&
- userHasNoBucketCreated &&
- accountSettings?.object_storage === 'active';
+ // TODO: Cover all the cases
+ const pageTitleText = isLandingPageShown
+ ? 'Create a Bucket'
+ : 'Object Storage';
- const shouldHideDocsAndCreateButtons =
- !areBucketsLoading &&
- tabIndex === bucketsTabIndex &&
- userHasNoBucketCreated;
-
- const isAccessKeysTab = tabIndex === accessKeysTabIndex;
-
- const createButtonText = isAccessKeysTab
+ const createButtonText = isAccessKeysOpened
? 'Create Access Key'
: 'Create Bucket';
- const openDrawer = useOpenClose();
-
- const handleOpenAccessDrawer = (mode: MODE) => {
- setMode(mode);
- openDrawer.open();
- };
-
const createButtonAction = () => {
- if (isAccessKeysTab) {
+ if (isAccessKeysOpened) {
navigate({ to: '/object-storage/access-keys/create' });
- handleOpenAccessDrawer('creating');
} else {
navigate({ to: '/object-storage/buckets/create' });
}
};
- const isSummaryOpened = match.routeId === '/object-storage/summary';
- const isCreateBucketOpen = match.routeId === '/object-storage/buckets/create';
- const isCreateAccessKeyOpen =
- match.routeId === '/object-storage/access-keys/create';
+ if (!isLandingPageShown && routeId === '/object-storage/') {
+ // TODO: Remove condition when OBJ Summary is enabled
+ navigate({
+ to: objSummaryPage
+ ? '/object-storage/summary'
+ : '/object-storage/buckets',
+ });
+ }
- // TODO: Remove when OBJ Summary is enabled
- if (match.routeId === '/object-storage/summary' && !objSummaryPage) {
- navigate({ to: '/object-storage/buckets' });
+ if (
+ isLandingPageShown &&
+ routeId !== '/object-storage/' &&
+ !routeId.endsWith('/create')
+ ) {
+ navigate({ to: '/object-storage' });
}
return (
-
-
-
-
-
-
-
- {objPromotionalOffers.map((promotionalOffer) => (
-
- ))}
- {shouldDisplayBillingNotice && }
-
- }>
-
- {objSummaryPage && (
-
-
-
- )}
-
-
-
-
- {
- 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' })}
- />
- )}
-
-
+ <>
+
+
+ {!isLandingPageShown && (
+
+ )}
+
+ {isLandingPageShown ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
);
};
-
-const StyledPromotionalOfferCard = styled(PromotionalOfferCard, {
- label: 'StyledPromotionalOfferCard',
-})(({ theme }) => ({
- marginBottom: theme.spacing(0.5),
-}));
diff --git a/packages/manager/src/features/ObjectStorage/ObjectStorageTabs.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageTabs.tsx
new file mode 100644
index 00000000000..f67ebc3dae0
--- /dev/null
+++ b/packages/manager/src/features/ObjectStorage/ObjectStorageTabs.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+
+import { SuspenseLoader } from 'src/components/SuspenseLoader';
+import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel';
+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 { useTabs } from 'src/hooks/useTabs';
+import { useObjectStorageBuckets } from 'src/queries/object-storage/queries';
+
+import { OMC_BucketLanding } from './BucketLanding/OMC_BucketLanding';
+import { BillingNotice } from './ObjectStorageBanners/BillingNotice';
+import { StyledPromotionalOfferCard } from './ObjectStorageBanners/StyledPromotionalOfferCard';
+
+import type { Tab } from 'src/hooks/useTabs';
+
+const SummaryLanding = React.lazy(() =>
+ import('./SummaryLanding/SummaryLanding').then((module) => ({
+ default: module.SummaryLanding,
+ }))
+);
+const AccessKeyLanding = React.lazy(() =>
+ import('./AccessKeyLanding/AccessKeyLanding').then((module) => ({
+ default: module.AccessKeyLanding,
+ }))
+);
+
+export const ObjectStorageTabs = () => {
+ const { promotionalOffers, objSummaryPage } = useFlags();
+
+ const objPromotionalOffers =
+ promotionalOffers?.filter((offer) =>
+ offer.features.includes('Object Storage')
+ ) ?? [];
+
+ const objTabs: Tab[] = [
+ { title: 'Buckets', to: '/object-storage/buckets' },
+ { title: 'Access Keys', to: '/object-storage/access-keys' },
+ ];
+
+ // TODO: Remove condition when OBJ Summary is enabled
+ if (objSummaryPage) {
+ objTabs.unshift({ title: 'Summary', to: '/object-storage/summary' });
+ }
+
+ const { handleTabChange, tabIndex, tabs, getTabIndex } = useTabs(objTabs);
+
+ const summaryTabIndex = getTabIndex('/object-storage/summary');
+ const bucketsTabIndex = getTabIndex('/object-storage/buckets');
+ const accessKeysTabIndex = getTabIndex('/object-storage/access-keys');
+
+ const {
+ data: objectStorageBucketsResponse,
+ error: bucketsErrors,
+ isLoading: areBucketsLoading,
+ } = useObjectStorageBuckets();
+
+ const userHasNoBucketCreated =
+ objectStorageBucketsResponse?.buckets.length === 0;
+
+ // Users must explicitly cancel Object Storage in their Account Settings to avoid being billed.
+ // Display a warning if the service is active but no buckets are present.
+ const shouldDisplayBillingNotice =
+ !areBucketsLoading && !bucketsErrors && userHasNoBucketCreated;
+
+ return (
+
+
+
+ {objPromotionalOffers.map((promotionalOffer) => (
+
+ ))}
+
+ {shouldDisplayBillingNotice && }
+
+ }>
+
+ {objSummaryPage && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/manager/src/queries/object-storage/queries.ts b/packages/manager/src/queries/object-storage/queries.ts
index 7670e05273b..83ac0b57678 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,
@@ -12,6 +13,7 @@ import {
getSSLCert,
updateBucketAccess,
updateObjectACL,
+ updateObjectStorageKey,
uploadSSLCert,
} from '@linode/api-v4';
import {
@@ -50,6 +52,7 @@ import type {
APIError,
CreateObjectStorageBucketPayload,
CreateObjectStorageBucketSSLPayload,
+ CreateObjectStorageKeyPayload,
CreateObjectStorageObjectURLPayload,
ObjectStorageBucket,
ObjectStorageBucketAccess,
@@ -64,6 +67,7 @@ import type {
PriceType,
ResourcePage,
UpdateObjectStorageBucketAccessPayload,
+ UpdateObjectStorageKeyPayload,
} from '@linode/api-v4';
export const objectStorageQueries = createQueryKeys('object-storage', {
@@ -118,6 +122,81 @@ 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) => {
+ const queryClient = useQueryClient();
+
+ if (id === -1) {
+ 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,
+ });
+ },
+ });
+};
+
+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,
+ });
+ },
+ });
+};
+
export const useObjectStorageEndpoints = (enabled = true) => {
const flags = useFlags();
const { data: account } = useAccount();
@@ -196,12 +275,6 @@ export const useObjectStorageBuckets = (enabled: boolean = true) => {
};
};
-export const useObjectStorageAccessKeys = (params: Params) =>
- useQuery, APIError[]>({
- ...objectStorageQueries.accessKeys(params),
- placeholderData: keepPreviousData,
- });
-
export const useBucketAccess = (
clusterOrRegion: string,
bucket: string,
@@ -286,6 +359,11 @@ export const useCreateBucketMutation = () => {
queryKey: accountQueries.settings.queryKey,
});
+ // Invalidate endpoints in order to update summary page multiselect.
+ queryClient.invalidateQueries({
+ queryKey: objectStorageQueries.endpoints.queryKey,
+ });
+
// Add the new bucket to the cache
queryClient.setQueryData(
objectStorageQueries.buckets.queryKey,
@@ -328,6 +406,11 @@ export const useDeleteBucketMutation = () => {
errors: oldData?.errors ?? [],
})
);
+
+ // Invalidate endpoints in order to update summary page multiselect.
+ queryClient.invalidateQueries({
+ queryKey: objectStorageQueries.endpoints.queryKey,
+ });
},
});
};
@@ -356,6 +439,11 @@ export const useDeleteBucketWithRegionMutation = () => {
errors: oldData?.errors ?? [],
})
);
+
+ // Invalidate endpoints in order to update summary page multiselect.
+ queryClient.invalidateQueries({
+ queryKey: objectStorageQueries.endpoints.queryKey,
+ });
},
});
};
diff --git a/packages/manager/src/routes/objectStorage/index.ts b/packages/manager/src/routes/objectStorage/index.ts
index ad2d05b180c..20ec54fc31a 100644
--- a/packages/manager/src/routes/objectStorage/index.ts
+++ b/packages/manager/src/routes/objectStorage/index.ts
@@ -1,4 +1,4 @@
-import { createRoute, redirect } from '@tanstack/react-router';
+import { createRoute } from '@tanstack/react-router';
import { rootRoute } from '../root';
import { ObjectStorageRoute } from './ObjectStorageRoute';
@@ -14,9 +14,6 @@ export const objectStorageRoute = createRoute({
});
const objectStorageIndexRoute = createRoute({
- beforeLoad: async () => {
- throw redirect({ to: '/object-storage/summary' });
- },
getParentRoute: () => objectStorageRoute,
path: '/',
}).lazy(() =>
@@ -70,6 +67,30 @@ const objectStorageAccessKeyCreateRoute = createRoute({
)
);
+const objectStorageAccessKeyUpdateRoute = createRoute({
+ getParentRoute: () => objectStorageRoute,
+ path: 'access-keys/$accessKeyId/update',
+ parseParams: (params) => ({
+ accessKeyId: Number(params.accessKeyId),
+ }),
+}).lazy(() =>
+ import('src/features/ObjectStorage/objectStorageLandingLazyRoute').then(
+ (m) => m.objectStorageLandingLazyRoute
+ )
+);
+
+const objectStorageAccessKeyDetailsRoute = createRoute({
+ getParentRoute: () => objectStorageRoute,
+ path: 'access-keys/$accessKeyId/details',
+ 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',
@@ -123,7 +144,10 @@ export const objectStorageRouteTree = objectStorageRoute.addChildren([
objectStorageAccessKeysLandingRoute,
objectStorageBucketCreateRoute,
objectStorageAccessKeyCreateRoute,
+ objectStorageAccessKeyUpdateRoute,
+ objectStorageAccessKeyDetailsRoute,
]),
+
objectStorageBucketDetailRoute.addChildren([
objectStorageBucketDetailObjectsRoute,
objectStorageBucketDetailAccessRoute,