diff --git a/packages/manager/.changeset/pr-10189-upcoming-features-1707838240012.md b/packages/manager/.changeset/pr-10189-upcoming-features-1707838240012.md new file mode 100644 index 00000000000..9f90805efdc --- /dev/null +++ b/packages/manager/.changeset/pr-10189-upcoming-features-1707838240012.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Handle errors gracefully when OBJ Multi-Cluster feature flag is enabled without MSW ([#10189](https://github.com/linode/manager/pull/10189)) diff --git a/packages/manager/src/components/AccountActivation/AccountActivationError.tsx b/packages/manager/src/components/AccountActivation/AccountActivationError.tsx index ad56e0fcc4b..c5e76f969f5 100644 --- a/packages/manager/src/components/AccountActivation/AccountActivationError.tsx +++ b/packages/manager/src/components/AccountActivation/AccountActivationError.tsx @@ -23,10 +23,12 @@ const AccountActivationError = (props: CombinedProps) => { return ( - {getAPIErrorOrDefault( - props.errors, - 'Your account is not yet activated. Please reach out to support@linode.com for more information' - )[0].reason} + { + getAPIErrorOrDefault( + props.errors, + 'Your account is not yet activated. Please reach out to support@linode.com for more information' + )[0].reason + } ); }; diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx index 38d48b1d327..639b5a6d6ac 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyLanding.tsx @@ -30,8 +30,8 @@ import { AccessKeyDrawer } from './AccessKeyDrawer'; import { AccessKeyTable } from './AccessKeyTable/AccessKeyTable'; import { OMC_AccessKeyDrawer } from './OMC_AccessKeyDrawer'; import { RevokeAccessKeyDialog } from './RevokeAccessKeyDialog'; -import ViewPermissionsDrawer from './ViewPermissionsDrawer'; import { MODE, OpenAccessDrawer } from './types'; +import ViewPermissionsDrawer from './ViewPermissionsDrawer'; interface Props { accessDrawerOpen: boolean; diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx index 70b112fc15b..241f0b43d77 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableBody.tsx @@ -1,15 +1,16 @@ -import React from 'react'; import { ObjectStorageKey, RegionS3EndpointAndID, } from '@linode/api-v4/lib/object-storage'; import { APIError } from '@linode/api-v4/lib/types'; +import React from 'react'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import { AccessKeyTableRows } from './AccessKeyTableRows'; + import { OpenAccessDrawer } from '../types'; +import { AccessKeyTableRow } from './AccessKeyTableRow'; type Props = { data: ObjectStorageKey[] | undefined; @@ -52,13 +53,18 @@ export const AccessKeyTableBody = ({ return ( <> {data && data.length > 0 ? ( - + <> + {data.map((eachKey: ObjectStorageKey, index) => ( + + ))} + ) : ( )} diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRow.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRow.tsx new file mode 100644 index 00000000000..828570409dd --- /dev/null +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRow.tsx @@ -0,0 +1,84 @@ +import { + ObjectStorageKey, + RegionS3EndpointAndID, +} from '@linode/api-v4/lib/object-storage'; +import { styled } from '@mui/material/styles'; +import React from 'react'; + +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { Typography } from 'src/components/Typography'; +import { useAccountManagement } from 'src/hooks/useAccountManagement'; +import { useFlags } from 'src/hooks/useFlags'; +import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; + +import { OpenAccessDrawer } from '../types'; +import { AccessKeyActionMenu } from './AccessKeyActionMenu'; +import { HostNameTableCell } from './HostNameTableCell'; + +type Props = { + openDrawer: OpenAccessDrawer; + openRevokeDialog: (storageKeyData: ObjectStorageKey) => void; + setHostNames: (hostNames: RegionS3EndpointAndID[]) => void; + setShowHostNamesDrawers: (show: boolean) => void; + storageKeyData: ObjectStorageKey; +}; + +export const AccessKeyTableRow = ({ + openDrawer, + openRevokeDialog, + setHostNames, + setShowHostNamesDrawers, + storageKeyData, +}: Props) => { + const { account } = useAccountManagement(); + const flags = useFlags(); + + const isObjMultiClusterEnabled = isFeatureEnabled( + 'Object Storage Access Key Regions', + Boolean(flags.objMultiCluster), + account?.capabilities ?? [] + ); + + return ( + + + + {storageKeyData.label} + + + + + {storageKeyData.access_key} + + + + {isObjMultiClusterEnabled && ( + + )} + + + + + + ); +}; + +const StyledCopyIcon = styled(CopyTooltip)(({ theme }) => ({ + '& svg': { + height: 12, + top: 1, + width: 12, + }, + marginLeft: theme.spacing(), +})); diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRows.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRows.tsx deleted file mode 100644 index e07d3083434..00000000000 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/AccessKeyTableRows.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { - ObjectStorageKey, - RegionS3EndpointAndID, -} from '@linode/api-v4/lib/object-storage'; - -import { styled } from '@mui/material/styles'; -import React from 'react'; - -import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; -import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; - -import { TableCell } from 'src/components/TableCell'; - -import { TableRow } from 'src/components/TableRow'; -import { Typography } from 'src/components/Typography'; -import { useAccountManagement } from 'src/hooks/useAccountManagement'; -import { useFlags } from 'src/hooks/useFlags'; -import { useRegionsQuery } from 'src/queries/regions'; -import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; -import { getRegionsByRegionId } from 'src/utilities/regions'; - -import { AccessKeyActionMenu } from './AccessKeyActionMenu'; -import { OpenAccessDrawer } from '../types'; - -type Props = { - objectStorageKeys: ObjectStorageKey[]; - openDrawer: OpenAccessDrawer; - openRevokeDialog: (objectStorageKey: ObjectStorageKey) => void; - setHostNames: (hostNames: RegionS3EndpointAndID[]) => void; - setShowHostNamesDrawers: (show: boolean) => void; -}; - -export const AccessKeyTableRows = ({ - objectStorageKeys, - openDrawer, - openRevokeDialog, - setHostNames, - setShowHostNamesDrawers, -}: Props) => { - const { account } = useAccountManagement(); - const { data: regionsData } = useRegionsQuery(); - const flags = useFlags(); - - const regionsLookup = regionsData && getRegionsByRegionId(regionsData); - - const isObjMultiClusterEnabled = isFeatureEnabled( - 'Object Storage Access Key Regions', - Boolean(flags.objMultiCluster), - account?.capabilities ?? [] - ); - - return ( - <> - {objectStorageKeys.map((eachKey: ObjectStorageKey) => ( - - - - {eachKey.label} - - - - - {eachKey.access_key} - - - - {isObjMultiClusterEnabled && regionsLookup && ( - - {`${regionsLookup[eachKey?.regions[0]?.id].label}: ${ - eachKey?.regions[0]?.s3_endpoint - } `} - {eachKey?.regions?.length === 1 && ( - - )} - {eachKey.regions.length > 1 && ( - { - setHostNames(eachKey.regions); - setShowHostNamesDrawers(true); - }} - type="button" - > - and {eachKey.regions.length - 1} more... - - )} - - )} - - - - - ))} - - ); -}; - -const StyledCopyIcon = styled(CopyTooltip)(({ theme }) => ({ - '& svg': { - height: 12, - top: 1, - width: 12, - }, - marginLeft: theme.spacing(), -})); diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx new file mode 100644 index 00000000000..e1ece753526 --- /dev/null +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx @@ -0,0 +1,65 @@ +import { + ObjectStorageKey, + RegionS3EndpointAndID, +} from '@linode/api-v4/lib/object-storage'; +import { styled } from '@mui/material/styles'; +import React from 'react'; + +import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; +import { TableCell } from 'src/components/TableCell'; +import { useRegionsQuery } from 'src/queries/regions'; +import { getRegionsByRegionId } from 'src/utilities/regions'; + +type Props = { + setHostNames: (hostNames: RegionS3EndpointAndID[]) => void; + setShowHostNamesDrawers: (show: boolean) => void; + storageKeyData: ObjectStorageKey; +}; + +export const HostNameTableCell = ({ + setHostNames, + setShowHostNamesDrawers, + storageKeyData, +}: Props) => { + const { data: regionsData } = useRegionsQuery(); + + const regionsLookup = regionsData && getRegionsByRegionId(regionsData); + + const { regions } = storageKeyData; + + if (!regionsLookup || !regionsData || !regions) { + return ; + } + + return ( + + {`${regionsLookup[storageKeyData.regions[0].id].label}: ${ + storageKeyData?.regions[0]?.s3_endpoint + } `} + {storageKeyData?.regions?.length === 1 && ( + + )} + {storageKeyData.regions.length > 1 && ( + { + setHostNames(storageKeyData.regions); + setShowHostNamesDrawers(true); + }} + type="button" + > + and {storageKeyData.regions.length - 1} more... + + )} + + ); +}; + +const StyledCopyIcon = styled(CopyTooltip)(({ theme }) => ({ + '& svg': { + height: 12, + top: 1, + width: 12, + }, + marginLeft: theme.spacing(), +})); diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/OMC_AccessKeyDrawer.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/OMC_AccessKeyDrawer.tsx index 6231553e59a..c65e752c2a2 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/OMC_AccessKeyDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/OMC_AccessKeyDrawer.tsx @@ -174,10 +174,12 @@ export const OMC_AccessKeyDrawer = (props: AccessKeyDrawerProps) => { : updateObjectStorageKeysSchema, }); + // @TODO OBJ Multicluster: The objectStorageKey check is a temporary fix to handle error cases when the feature flag is enabled without Mock Service Worker (MSW). This can be removed during the feature flag cleanup. const isSaveDisabled = isRestrictedUser || (mode !== 'creating' && objectStorageKey && + objectStorageKey?.regions?.length > 0 && !hasLabelOrRegionsChanged(formik.values, objectStorageKey)); const beforeSubmit = () => { diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx index b54025501cb..bd597a4a468 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx @@ -92,7 +92,10 @@ describe('CreateBucketDrawer', () => { getByTestId, } = renderWithTheme(, { queryClient }); - await userEvent.type(getByLabelText('Label', { exact: false }), 'my-test-bucket'); + 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(() => diff --git a/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx b/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx index aed649b7381..f49b977a3a0 100644 --- a/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx +++ b/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx @@ -76,46 +76,61 @@ export const SecretTokenDialog = (props: Props) => { spacingTop={8} variant="warning" /> - {isObjMultiClusterEnabled && ( -
- - `${regionsLookup?.[region.id]?.label}: ${ - region.s3_endpoint - }` - ) - .join('\n') ?? '' - } - /> -
- )} - {isObjMultiClusterEnabled && ( - ({ - backgroundColor: theme.bg.main, - border: `1px solid ${theme.color.grey3}`, - borderColor: theme.name === 'light' ? '#ccc' : '#222', - })} - > - {objectStorageKey?.regions.map((region, index) => ( - 0 && ( +
+ + `${regionsLookup?.[region.id]?.label}: ${ + region.s3_endpoint + }` + ) + .join('\n') ?? '' + } /> - ))} - - )} +
+ )} + {/* @TODO OBJ Multicluster: The objectStorageKey check is a temporary fix + to handle error cases when the feature flag is enabled without Mock + Service Worker (MSW). This can be removed during the feature flag cleanup. */} + {isObjMultiClusterEnabled && + objectStorageKey && + objectStorageKey?.regions?.length > 0 && ( + ({ + '.copyIcon': { + marginRight: 0, + paddingRight: 0, + }, + backgroundColor: theme.bg.main, + border: `1px solid ${theme.color.grey3}`, + borderColor: theme.name === 'light' ? '#ccc' : '#222', + padding: theme.spacing(1), + })} + > + {objectStorageKey?.regions.map((region, index) => ( + + ))} + + )} {objectStorageKey ? ( <>