From 1752e0130ebd41651c57ea61179fc5a0093a949e Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Mon, 26 Aug 2024 16:24:52 -0400 Subject: [PATCH 01/15] Update Linode interface to include capabilities property; add BLOCK_STORAGE_CLIENT_LIBRARY_UPDATE_REQUIRED_COPY; add Volume Encryption section to LinodeVolumeCreateForm --- packages/api-v4/src/linodes/types.ts | 2 +- .../src/components/Encryption/constants.tsx | 3 + .../VolumeDrawer/LinodeVolumeCreateForm.tsx | 73 ++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index 9eabb9f5e75..dcc4eaebba2 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -19,7 +19,7 @@ export interface Linode { id: number; alerts: LinodeAlerts; backups: LinodeBackups; - bs_encryption_supported?: boolean; // @TODO BSE: Remove optionality once BSE is fully rolled out + capabilities?: string[]; // @TODO BSE: Remove optionality once BSE is fully rolled out created: string; disk_encryption?: EncryptionStatus; // @TODO LDE: Remove optionality once LDE is fully rolled out region: string; diff --git a/packages/manager/src/components/Encryption/constants.tsx b/packages/manager/src/components/Encryption/constants.tsx index 4d93f9f3a8d..1d13014cf10 100644 --- a/packages/manager/src/components/Encryption/constants.tsx +++ b/packages/manager/src/components/Encryption/constants.tsx @@ -89,6 +89,9 @@ export const BLOCK_STORAGE_CHOOSE_REGION_COPY = export const BLOCK_STORAGE_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY = `Volume encryption is not available in the selected region. ${BLOCK_STORAGE_CHOOSE_REGION_COPY}`; +export const BLOCK_STORAGE_CLIENT_LIBRARY_UPDATE_REQUIRED_COPY = + 'This Linode requires a client library update and will need to be rebooted prior to attaching an encrypted volume.'; + // Caveats export const BLOCK_STORAGE_ENCRYPTION_OVERHEAD_CAVEAT = 'Please note encryption overhead may impact your volume IOPS performance negatively. This may compound when multiple encryption-enabled volumes are attached to the same Linode.'; diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx index cdb1adfc4a2..b99f32abcab 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx @@ -1,10 +1,17 @@ -import { APIError, Linode, Volume } from '@linode/api-v4'; import { CreateVolumeSchema } from '@linode/validation/lib/volumes.schema'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Box } from 'src/components/Box'; +import { + BLOCK_STORAGE_ENCRYPTION_GENERAL_DESCRIPTION, + BLOCK_STORAGE_ENCRYPTION_OVERHEAD_CAVEAT, + BLOCK_STORAGE_USER_SIDE_ENCRYPTION_CAVEAT, +} from 'src/components/Encryption/constants'; +import { Encryption } from 'src/components/Encryption/Encryption'; +import { useIsBlockStorageEncryptionFeatureEnabled } from 'src/components/Encryption/utils'; import { Notice } from 'src/components/Notice/Notice'; import { TagsInput } from 'src/components/TagsInput/TagsInput'; import { TextField } from 'src/components/TextField'; @@ -12,11 +19,13 @@ import { Typography } from 'src/components/Typography'; import { MAX_VOLUME_SIZE } from 'src/constants'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useEventsPollingActions } from 'src/queries/events/events'; +import { useRegionsQuery } from 'src/queries/regions/regions'; import { useCreateVolumeMutation, useVolumeTypesQuery, } from 'src/queries/volumes/volumes'; import { sendCreateVolumeEvent } from 'src/utilities/analytics/customEventAnalytics'; +import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { handleFieldErrors, @@ -29,6 +38,13 @@ import { ConfigSelect } from './ConfigSelect'; import { PricePanel } from './PricePanel'; import { SizeField } from './SizeField'; +import type { + APIError, + Linode, + Volume, + VolumeEncryption, +} from '@linode/api-v4'; + interface Props { linode: Linode; onClose: () => void; @@ -37,6 +53,7 @@ interface Props { interface FormState { config_id: number; + encryption: VolumeEncryption | undefined; label: string; linode_id: number; region: string; @@ -46,6 +63,7 @@ interface FormState { const initialValues: FormState = { config_id: -1, + encryption: 'disabled', label: '', linode_id: -1, region: 'none', @@ -55,6 +73,7 @@ const initialValues: FormState = { export const LinodeVolumeCreateForm = (props: Props) => { const { linode, onClose, openDetails } = props; + const { enqueueSnackbar } = useSnackbar(); const { mutateAsync: createVolume } = useCreateVolumeMutation(); @@ -66,6 +85,22 @@ export const LinodeVolumeCreateForm = (props: Props) => { globalGrantType: 'add_volumes', }); + const { + isBlockStorageEncryptionFeatureEnabled, + } = useIsBlockStorageEncryptionFeatureEnabled(); + + const { data: regions } = useRegionsQuery(); + + const toggleVolumeEncryptionEnabled = ( + encryption: VolumeEncryption | undefined + ) => { + if (encryption === 'enabled') { + setFieldValue('encryption', 'disabled'); + } else { + setFieldValue('encryption', 'enabled'); + } + }; + const isInvalidPrice = !types || isError; const { @@ -82,16 +117,25 @@ export const LinodeVolumeCreateForm = (props: Props) => { } = useFormik({ initialValues, async onSubmit(values, { setErrors, setStatus }) { - const { config_id, label, size, tags } = values; + const { config_id, encryption, label, size, tags } = values; /** Status holds our a general error message */ setStatus(undefined); + // If the BSE feature is not enabled or the selected region does not support BSE, set `encryption` in the payload to undefined. + // Otherwise, set it to `enabled` if the checkbox is checked, or `disabled` if it is not + const blockStorageEncryptionPayloadValue = + !isBlockStorageEncryptionFeatureEnabled || + !regionSupportsBlockStorageEncryption + ? undefined + : encryption; + try { const volume = await createVolume({ config_id: // If the config_id still set to default value of -1, set this to undefined, so volume gets created on back-end according to the API logic config_id === -1 ? undefined : maybeCastToNumber(config_id), + encryption: blockStorageEncryptionPayloadValue, label, linode_id: maybeCastToNumber(linode.id), size: maybeCastToNumber(size), @@ -117,6 +161,12 @@ export const LinodeVolumeCreateForm = (props: Props) => { validationSchema: CreateVolumeSchema, }); + const regionSupportsBlockStorageEncryption = doesRegionSupportFeature( + linode.region, + regions ?? [], + 'Block Storage Encryption' + ); + return (
{isVolumesGrantReadOnly && ( @@ -203,6 +253,25 @@ export const LinodeVolumeCreateForm = (props: Props) => { name="tags" value={values.tags.map((tag) => ({ label: tag, value: tag }))} /> + {isBlockStorageEncryptionFeatureEnabled && ( + + toggleVolumeEncryptionEnabled(values.encryption)} + /> + + )} Date: Tue, 27 Aug 2024 13:50:22 -0400 Subject: [PATCH 02/15] Conditional display of client library copy in Create & Attach view, disable Create Volume when user needs to reboot linode for client library update --- .../src/components/Encryption/constants.tsx | 3 ++ .../VolumeDrawer/LinodeVolumeAddDrawer.tsx | 34 ++++++++++++++++++- .../VolumeDrawer/LinodeVolumeAttachForm.tsx | 5 +-- .../VolumeDrawer/LinodeVolumeCreateForm.tsx | 24 +++++++++++-- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/components/Encryption/constants.tsx b/packages/manager/src/components/Encryption/constants.tsx index 1d13014cf10..0b49c80e40d 100644 --- a/packages/manager/src/components/Encryption/constants.tsx +++ b/packages/manager/src/components/Encryption/constants.tsx @@ -87,6 +87,9 @@ export const BLOCK_STORAGE_ENCRYPTION_GENERAL_DESCRIPTION = ( export const BLOCK_STORAGE_CHOOSE_REGION_COPY = 'Select a region to use Volume encryption.'; +export const BLOCK_STORAGE_ENCRYPTION_UNAVAILABLE_IN_LINODE_REGION_COPY = + "Volume encryption is not available in this Linode's region."; + export const BLOCK_STORAGE_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY = `Volume encryption is not available in the selected region. ${BLOCK_STORAGE_CHOOSE_REGION_COPY}`; export const BLOCK_STORAGE_CLIENT_LIBRARY_UPDATE_REQUIRED_COPY = diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx index 328d0ce91b9..f60d09cd50c 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx @@ -1,12 +1,17 @@ -import { Linode, Volume } from '@linode/api-v4'; import * as React from 'react'; import { Drawer } from 'src/components/Drawer'; +import { BLOCK_STORAGE_CLIENT_LIBRARY_UPDATE_REQUIRED_COPY } from 'src/components/Encryption/constants'; +import { useIsBlockStorageEncryptionFeatureEnabled } from 'src/components/Encryption/utils'; +import { Notice } from 'src/components/Notice/Notice'; +import { Typography } from 'src/components/Typography'; import { LinodeVolumeAttachForm } from './LinodeVolumeAttachForm'; import { LinodeVolumeCreateForm } from './LinodeVolumeCreateForm'; import { ModeSelection } from './ModeSelection'; +import type { Linode, Volume } from '@linode/api-v4'; + interface Props { linode: Linode; onClose: () => void; @@ -18,6 +23,18 @@ export const LinodeVolumeAddDrawer = (props: Props) => { const { linode, onClose, open, openDetails } = props; const [mode, setMode] = React.useState<'attach' | 'create'>('create'); + const [ + clientLibraryCopyVisible, + setClientLibraryCopyVisible, + ] = React.useState(false); + + const { + isBlockStorageEncryptionFeatureEnabled, + } = useIsBlockStorageEncryptionFeatureEnabled(); + + const linodeSupportsBlockStorageEncryption = linode.capabilities?.includes( + 'blockstorage_encryption' + ); return ( { open={open} > + {isBlockStorageEncryptionFeatureEnabled && + !linodeSupportsBlockStorageEncryption && + clientLibraryCopyVisible && ( + + + {BLOCK_STORAGE_CLIENT_LIBRARY_UPDATE_REQUIRED_COPY} + + + )} {mode === 'attach' ? ( ) : ( + setClientLibraryCopyVisible(visible) + } linode={linode} onClose={onClose} openDetails={openDetails} diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx index e7a45c3b3e8..7b169e58eab 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx @@ -1,5 +1,3 @@ -import { Linode } from '@linode/api-v4'; -import { Grant } from '@linode/api-v4/lib/account'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; @@ -18,6 +16,9 @@ import { import { ConfigSelect } from './ConfigSelect'; import { VolumeSelect } from './VolumeSelect'; +import type { Linode } from '@linode/api-v4'; +import type { Grant } from '@linode/api-v4/lib/account'; + interface Props { linode: Linode; onClose: () => void; diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx index b99f32abcab..63a076abc8a 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx @@ -8,6 +8,7 @@ import { Box } from 'src/components/Box'; import { BLOCK_STORAGE_ENCRYPTION_GENERAL_DESCRIPTION, BLOCK_STORAGE_ENCRYPTION_OVERHEAD_CAVEAT, + BLOCK_STORAGE_ENCRYPTION_UNAVAILABLE_IN_LINODE_REGION_COPY, BLOCK_STORAGE_USER_SIDE_ENCRYPTION_CAVEAT, } from 'src/components/Encryption/constants'; import { Encryption } from 'src/components/Encryption/Encryption'; @@ -47,8 +48,10 @@ import type { interface Props { linode: Linode; + linodeSupportsBlockStorageEncryption: boolean | undefined; onClose: () => void; openDetails: (volume: Volume) => void; + setClientLibraryCopyVisible: (visible: boolean) => void; } interface FormState { @@ -72,7 +75,13 @@ const initialValues: FormState = { }; export const LinodeVolumeCreateForm = (props: Props) => { - const { linode, onClose, openDetails } = props; + const { + linode, + linodeSupportsBlockStorageEncryption, + onClose, + openDetails, + setClientLibraryCopyVisible, + } = props; const { enqueueSnackbar } = useSnackbar(); @@ -96,8 +105,10 @@ export const LinodeVolumeCreateForm = (props: Props) => { ) => { if (encryption === 'enabled') { setFieldValue('encryption', 'disabled'); + setClientLibraryCopyVisible(false); } else { setFieldValue('encryption', 'enabled'); + setClientLibraryCopyVisible(true); } }; @@ -256,6 +267,9 @@ export const LinodeVolumeCreateForm = (props: Props) => { {isBlockStorageEncryptionFeatureEnabled && ( { : [] } descriptionCopy={BLOCK_STORAGE_ENCRYPTION_GENERAL_DESCRIPTION} - disabled={false} // Linode region does not support Volume Encryption + disabled={!regionSupportsBlockStorageEncryption} entityType="Volume" isEncryptEntityChecked={values.encryption === 'enabled'} onChange={() => toggleVolumeEncryptionEnabled(values.encryption)} @@ -279,7 +293,11 @@ export const LinodeVolumeCreateForm = (props: Props) => { /> Date: Tue, 27 Aug 2024 14:00:26 -0400 Subject: [PATCH 03/15] Clear client library copy when user toggles view inside Volume Add drawer --- .../Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx index f60d09cd50c..124600b70eb 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx @@ -36,6 +36,11 @@ export const LinodeVolumeAddDrawer = (props: Props) => { 'blockstorage_encryption' ); + const toggleMode = (mode: 'attach' | 'create') => { + setMode(mode); + setClientLibraryCopyVisible(false); + }; + return ( { onClose={onClose} open={open} > - + {isBlockStorageEncryptionFeatureEnabled && !linodeSupportsBlockStorageEncryption && clientLibraryCopyVisible && ( From c143d1fcae3c5fc9e783cfe0a1bcc59d28366f59 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Tue, 27 Aug 2024 15:25:16 -0400 Subject: [PATCH 04/15] Reintroduce useVolumeQuery; logic in LinodeVolumeAttachForm to control display of client library copy; minor docs update --- docs/tooling/react-query.md | 8 ++--- .../VolumeDrawer/LinodeVolumeAddDrawer.tsx | 8 ++++- .../VolumeDrawer/LinodeVolumeAttachForm.tsx | 18 +++++++++-- .../manager/src/queries/volumes/volumes.ts | 31 ++++++++++++++----- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/docs/tooling/react-query.md b/docs/tooling/react-query.md index e26cef8fb41..e4ecc2a8137 100644 --- a/docs/tooling/react-query.md +++ b/docs/tooling/react-query.md @@ -161,14 +161,14 @@ export const useCreateLinodeMutation = () => { ## Frequently Asked Questions -### Are we storing dupdate data in the cache? Why? +### Are we storing duplicated data in the cache? Why? Yes, there is potential for the same data to exist many times in the cache. For example, we have a query `useVolumesQuery` with the query key `["volumes", "paginated", { page: 1 }]` that contains the first 100 volumes on your account. -One of those same volumes could also be stored in the cache by using `useVolumeQuery` with query key `["linodes", "linode", 5]`. -This creates a senerio where the same volume is cached by React Query under multiple query keys. +One of those same volumes could also be stored in the cache by using `useVolumeQuery` with query key `["volumes", "volume", 5]`. +This creates a scenario where the same volume is cached by React Query under multiple query keys. -This is a legitimate disadvantage of React Query's caching strategy. **We must be aware of this when we perform cache updates (using invalidations or manually updating the cache) so that the entity is update everywhere in the cache.** +This is a legitimate disadvantage of React Query's caching strategy. **We must be aware of this when we perform cache updates (using invalidations or manually updating the cache) so that the entity is updated everywhere in the cache.** Some data fetching tools like Apollo Client are able to intelligently detect duplicate entities and merge them. React Query does not do this. See [this tweet](https://twitter.com/tannerlinsley/status/1557395389531074560) from the creator of React Query. \ No newline at end of file diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx index 124600b70eb..b2cba17258c 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx @@ -62,7 +62,13 @@ export const LinodeVolumeAddDrawer = (props: Props) => { )} {mode === 'attach' ? ( - + + setClientLibraryCopyVisible(visible) + } + linode={linode} + onClose={onClose} + /> ) : ( void; readOnly?: boolean; + setClientLibraryCopyVisible: (visible: boolean) => void; } /** @@ -41,7 +45,7 @@ const AttachVolumeValidationSchema = object({ const initialValues = { config_id: -1, volume_id: -1 }; export const LinodeVolumeAttachForm = (props: Props) => { - const { linode, onClose } = props; + const { linode, onClose, setClientLibraryCopyVisible } = props; const { data: grants } = useGrants(); @@ -94,6 +98,16 @@ export const LinodeVolumeAttachForm = (props: Props) => { validationSchema: AttachVolumeValidationSchema, }); + const { data: volume } = useVolumeQuery(values.volume_id); + + React.useEffect(() => { + // When the volume is encrypted but the linode requires a client library update, we want to show the client library copy + setClientLibraryCopyVisible( + volume?.encryption === 'enabled' && + !linode.capabilities?.includes('blockstorage_encryption') + ); + }, [volume]); + return ( {isReadOnly && ( diff --git a/packages/manager/src/queries/volumes/volumes.ts b/packages/manager/src/queries/volumes/volumes.ts index 0476d622e1d..3cdb5144784 100644 --- a/packages/manager/src/queries/volumes/volumes.ts +++ b/packages/manager/src/queries/volumes/volumes.ts @@ -1,23 +1,16 @@ import { - AttachVolumePayload, - CloneVolumePayload, - ResizeVolumePayload, - UpdateVolumeRequest, - Volume, - VolumeRequestPayload, attachVolume, cloneVolume, createVolume, deleteVolume, detachVolume, getLinodeVolumes, + getVolume, getVolumes, migrateVolumes, resizeVolume, updateVolume, } from '@linode/api-v4'; -import { APIError, ResourcePage } from '@linode/api-v4/lib/types'; -import { Filter, Params, PriceType } from '@linode/api-v4/src/types'; import { createQueryKeys } from '@lukemorales/query-key-factory'; import { useInfiniteQuery, @@ -31,6 +24,17 @@ import { queryPresets } from '../base'; import { profileQueries } from '../profile/profile'; import { getAllVolumeTypes, getAllVolumes } from './requests'; +import type { + AttachVolumePayload, + CloneVolumePayload, + ResizeVolumePayload, + UpdateVolumeRequest, + Volume, + VolumeRequestPayload, +} from '@linode/api-v4'; +import type { APIError, ResourcePage } from '@linode/api-v4/lib/types'; +import type { Filter, Params, PriceType } from '@linode/api-v4/src/types'; + export const volumeQueries = createQueryKeys('volumes', { linode: (linodeId: number) => ({ contextQueries: { @@ -63,8 +67,19 @@ export const volumeQueries = createQueryKeys('volumes', { queryFn: getAllVolumeTypes, queryKey: null, }, + volume: (id: number) => ({ + queryFn: () => getVolume(id), + queryKey: [id], + }), }); +export const useVolumeQuery = (id: number, enabled = true) => { + return useQuery({ + ...volumeQueries.volume(id), + enabled, + }); +}; + export const useVolumesQuery = (params: Params, filter: Filter) => useQuery, APIError[]>({ ...volumeQueries.lists._ctx.paginated(params, filter), From 3b87504c9e58c24de1bf7bfda260ecfaf640591e Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Thu, 29 Aug 2024 16:17:31 -0400 Subject: [PATCH 05/15] Linter, util, factory updates; test coverage; VolumeSelect.tsx refactor to use custom Autocomplete instead of MUI Autocomplete directly --- .../e2e/core/volumes/create-volume.spec.ts | 94 ++++++++++++++++++- .../manager/cypress/support/util/linodes.ts | 46 ++++----- .../components/Autocomplete/Autocomplete.tsx | 19 ++-- packages/manager/src/factories/linodes.ts | 1 + .../LinodeVolumeAddDrawer.test.tsx | 43 +++++++++ .../VolumeDrawer/LinodeVolumeAddDrawer.tsx | 4 +- .../VolumeDrawer/LinodeVolumeAttachForm.tsx | 3 +- .../Volumes/VolumeDrawer/VolumeSelect.tsx | 26 +++-- 8 files changed, 186 insertions(+), 50 deletions(-) create mode 100644 packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index 25115abf26e..c6b402f3177 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -1,13 +1,20 @@ -import type { Linode } from '@linode/api-v4'; +import type { Linode, Region } from '@linode/api-v4'; import { createTestLinode } from 'support/util/linodes'; import { createLinodeRequestFactory } from 'src/factories/linodes'; import { authenticate } from 'support/api/authentication'; import { cleanUp } from 'support/util/cleanup'; import { containsClick, fbtVisible, fbtClick, getClick } from 'support/helpers'; -import { interceptCreateVolume } from 'support/intercepts/volumes'; +import { + interceptCreateVolume, + mockGetVolumes, +} from 'support/intercepts/volumes'; import { randomNumber, randomString, randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { ui } from 'support/ui'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { accountFactory, regionFactory, volumeFactory } from 'src/factories'; +import { mockGetAccount } from 'support/intercepts/account'; +import { mockGetRegions } from 'support/intercepts/regions'; // Local storage override to force volume table to list up to 100 items. // This is a workaround while we wait to get stuck volumes removed. @@ -16,6 +23,18 @@ const pageSizeOverride = { PAGE_SIZE: 100, }; +const mockRegions: Region[] = [ + regionFactory.build({ + capabilities: ['Linodes', 'Block Storage', 'Block Storage Encryption'], + id: 'us-east', + label: 'Newark, NJ', + site_type: 'core', + }), +]; + +const CLIENT_LIBRARY_UPDATE_COPY = + 'This Linode requires a client library update and will need to be rebooted prior to attaching an encrypted volume.'; + authenticate(); describe('volume create flow', () => { before(() => { @@ -140,6 +159,77 @@ describe('volume create flow', () => { ); }); + /* + * - Checks for Block Storage Encryption notices in the Create/Attach Volume drawer from the + 'Storage' details page of an existing Linode. + */ + it('displays a warning notice re: rebooting for client library updates under the appropriate conditions', () => { + // Conditions: Block Storage encryption feature flag is on; user has Block Storage Encryption capability; Linode does not support Block Storage Encryption and the user is trying to attach an encrypted volume + + // Mock feature flag -- @TODO BSE: Remove feature flag once BSE is fully rolled out + mockAppendFeatureFlags({ + blockStorageEncryption: true, + }).as('getFeatureFlags'); + + // Mock account response + const mockAccount = accountFactory.build({ + capabilities: ['Linodes', 'Block Storage Encryption'], + }); + + mockGetAccount(mockAccount).as('getAccount'); + mockGetRegions(mockRegions).as('getRegions'); + + const volume = volumeFactory.build({ + region: mockRegions[0].id, + encryption: 'enabled', + }); + + const linodeRequest = createLinodeRequestFactory.build({ + label: randomLabel(), + root_pass: randomString(16), + region: mockRegions[0].id, + booted: false, + }); + + cy.defer(() => createTestLinode(linodeRequest), 'creating Linode').then( + (linode: Linode) => { + linode.capabilities?.map((capability) => cy.log(capability)); + mockGetVolumes([volume]).as('getVolumes'); + + cy.visitWithLogin(`/linodes/${linode.id}/storage`); + cy.wait(['@getFeatureFlags', '@getAccount']); + + // Click "Create Volume" button + cy.findByText('Create Volume').click(); + + cy.get('[data-qa-drawer="true"]').within(() => { + cy.get('[data-qa-checked]').should('be.visible').click(); + }); + + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); + + // Ensure notice is cleared when switching views in drawer + cy.get('[data-qa-radio="Attach Existing Volume"]').click(); + cy.wait(['@getVolumes']); + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + + cy.log(volume.encryption ?? 'vol encryption disabled'); + + // Ensure notice is displayed in "Attach Existing Volume" view when an encrypted volume is selected + cy.findByPlaceholderText('Select a Volume') + .should('be.visible') + .click() + .type(`${volume.label}{downarrow}{enter}`); + ui.autocompletePopper + .findByTitle(volume.label) + .should('be.visible') + .click(); + + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); + } + ); + }); + /* * - Creates a volume from the 'Storage' details page of an existing Linode. * - Confirms that volume is listed correctly on Linode 'Storage' details page. diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index ad6e6b538d4..9bbe9ad4b9f 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -1,11 +1,13 @@ import { createLinode, getLinodeConfigs } from '@linode/api-v4'; import { createLinodeRequestFactory } from '@src/factories'; +import { findOrCreateDependencyFirewall } from 'support/api/firewalls'; +import { pageSize } from 'support/constants/api'; import { SimpleBackoffMethod } from 'support/util/backoff'; import { pollLinodeDiskStatuses, pollLinodeStatus } from 'support/util/polling'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; + import { depaginate } from './paginate'; -import { pageSize } from 'support/constants/api'; import type { Config, @@ -13,17 +15,16 @@ import type { InterfacePayload, Linode, } from '@linode/api-v4'; -import { findOrCreateDependencyFirewall } from 'support/api/firewalls'; /** * Linode create interface to configure a Linode with no public internet access. */ export const linodeVlanNoInternetConfig: InterfacePayload[] = [ { - purpose: 'vlan', - primary: false, - label: randomLabel(), ipam_address: null, + label: randomLabel(), + primary: false, + purpose: 'vlan', }, ]; @@ -40,30 +41,30 @@ export const linodeVlanNoInternetConfig: InterfacePayload[] = [ */ export type CreateTestLinodeSecurityMethod = | 'firewall' - | 'vlan_no_internet' - | 'powered_off'; + | 'powered_off' + | 'vlan_no_internet'; /** * Options to control the behavior of test Linode creation. */ export interface CreateTestLinodeOptions { - /** Whether to wait for created Linode disks to be available before resolving. */ - waitForDisks: boolean; + /** Method to use to secure the test Linode. */ + securityMethod: CreateTestLinodeSecurityMethod; /** Whether to wait for created Linode to boot before resolving. */ waitForBoot: boolean; - /** Method to use to secure the test Linode. */ - securityMethod: CreateTestLinodeSecurityMethod; + /** Whether to wait for created Linode disks to be available before resolving. */ + waitForDisks: boolean; } /** * Default test Linode creation options. */ export const defaultCreateTestLinodeOptions = { - waitForDisks: false, - waitForBoot: false, securityMethod: 'firewall', + waitForBoot: false, + waitForDisks: false, }; /** @@ -106,20 +107,20 @@ export const createTestLinode = async ( const resolvedCreatePayload = { ...createLinodeRequestFactory.build({ - label: randomLabel(), + booted: false, image: 'linode/debian11', + label: randomLabel(), region: chooseRegion().id, - booted: false, }), ...(createRequestPayload || {}), ...securityMethodPayload, // Override given root password; mitigate against using default factory password, inadvertent logging, etc. root_pass: randomString(64, { + lowercase: true, + numbers: true, spaces: true, symbols: true, - numbers: true, - lowercase: true, uppercase: true, }), }; @@ -169,21 +170,24 @@ export const createTestLinode = async ( } Cypress.log({ - name: 'createTestLinode', - message: `Create Linode '${linode.label}' (ID ${linode.id})`, consoleProps: () => { return { + linode, options: resolvedOptions, payload: { ...resolvedCreatePayload, root_pass: '(redacted)', }, - linode, }; }, + message: `Create Linode '${linode.label}' (ID ${linode.id})`, + name: 'createTestLinode', }); - return linode; + return { + ...linode, + capabilities: [], + }; }; /** diff --git a/packages/manager/src/components/Autocomplete/Autocomplete.tsx b/packages/manager/src/components/Autocomplete/Autocomplete.tsx index e8644fbce4d..de989e277c0 100644 --- a/packages/manager/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/manager/src/components/Autocomplete/Autocomplete.tsx @@ -4,7 +4,7 @@ import MuiAutocomplete from '@mui/material/Autocomplete'; import React from 'react'; import { Box } from 'src/components/Box'; -import { TextField, TextFieldProps } from 'src/components/TextField'; +import { TextField } from 'src/components/TextField'; import { CircleProgress } from '../CircleProgress'; import { InputAdornment } from '../InputAdornment'; @@ -15,6 +15,7 @@ import { } from './Autocomplete.styles'; import type { AutocompleteProps } from '@mui/material/Autocomplete'; +import type { TextFieldProps } from 'src/components/TextField'; export interface EnhancedAutocompleteProps< T extends { label: string }, @@ -25,6 +26,8 @@ export interface EnhancedAutocompleteProps< AutocompleteProps, 'renderInput' > { + /** Removes "select all" option for multiselect */ + disableSelectAll?: boolean; /** Provides a hint with error styling to assist users. */ errorText?: string; /** Provides a hint with normal styling to assist users. */ @@ -38,8 +41,6 @@ export interface EnhancedAutocompleteProps< placeholder?: string; /** Label for the "select all" option. */ selectAllLabel?: string; - /** Removes "select all" option for mutliselect */ - disableSelectAll?: boolean; textFieldProps?: Partial; } @@ -71,6 +72,7 @@ export const Autocomplete = < clearOnBlur, defaultValue, disablePortal = true, + disableSelectAll = false, errorText = '', helperText, label, @@ -88,7 +90,6 @@ export const Autocomplete = < selectAllLabel = '', textFieldProps, value, - disableSelectAll = false, ...rest } = props; @@ -103,6 +104,11 @@ export const Autocomplete = < return ( 0 + ? optionsWithSelectAll + : options + } renderInput={(params) => ( You have no options to choose from} onBlur={onBlur} - options={ - multiple && !disableSelectAll && options.length > 0 - ? optionsWithSelectAll - : options - } popupIcon={} value={value} {...rest} diff --git a/packages/manager/src/factories/linodes.ts b/packages/manager/src/factories/linodes.ts index c6bc613b7e4..f5231ce7784 100644 --- a/packages/manager/src/factories/linodes.ts +++ b/packages/manager/src/factories/linodes.ts @@ -255,6 +255,7 @@ export const proDedicatedTypeFactory = Factory.Sync.makeFactory({ export const linodeFactory = Factory.Sync.makeFactory({ alerts: linodeAlertsFactory.build(), backups: linodeBackupsFactory.build(), + capabilities: [], created: '2020-01-01', disk_encryption: 'enabled', group: '', diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx new file mode 100644 index 00000000000..b948009db76 --- /dev/null +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx @@ -0,0 +1,43 @@ +import { waitFor } from '@testing-library/react'; +import * as React from 'react'; + +import { accountFactory, linodeFactory } from 'src/factories'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { LinodeVolumeAddDrawer } from './LinodeVolumeAddDrawer'; + +const accountEndpoint = '*/v4/account'; +const encryptionLabelText = 'Encrypt Volume'; + +describe('LinodeVolumeAddDrawer', () => { + /* @TODO BSE: Remove feature flagging/conditionality once BSE is fully rolled out */ + + it('should display a "Volume Encryption" section if the user has the account capability and the feature flag is on', async () => { + const linode = linodeFactory.build(); + + server.use( + http.get(accountEndpoint, () => { + return HttpResponse.json( + accountFactory.build({ capabilities: ['Block Storage Encryption'] }) + ); + }) + ); + + const { getByLabelText } = renderWithTheme( + , + { + flags: { blockStorageEncryption: true }, + } + ); + + await waitFor(() => { + expect(getByLabelText(encryptionLabelText)).not.toBeNull(); + }); + }); +}); diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx index b2cba17258c..a0125cb16b2 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx @@ -32,8 +32,8 @@ export const LinodeVolumeAddDrawer = (props: Props) => { isBlockStorageEncryptionFeatureEnabled, } = useIsBlockStorageEncryptionFeatureEnabled(); - const linodeSupportsBlockStorageEncryption = linode.capabilities?.includes( - 'blockstorage_encryption' + const linodeSupportsBlockStorageEncryption = Boolean( + linode.capabilities?.includes('blockstorage_encryption') ); const toggleMode = (mode: 'attach' | 'create') => { diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx index 247afef423a..b9e0b168281 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx @@ -104,7 +104,7 @@ export const LinodeVolumeAttachForm = (props: Props) => { // When the volume is encrypted but the linode requires a client library update, we want to show the client library copy setClientLibraryCopyVisible( volume?.encryption === 'enabled' && - !linode.capabilities?.includes('blockstorage_encryption') + Boolean(!linode.capabilities?.includes('blockstorage_encryption')) ); }, [volume]); @@ -123,6 +123,7 @@ export const LinodeVolumeAttachForm = (props: Props) => { setFieldValue('volume_id', v)} region={linode.region} diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/VolumeSelect.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/VolumeSelect.tsx index 8a5506e0cc1..a14555b732f 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/VolumeSelect.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/VolumeSelect.tsx @@ -1,12 +1,12 @@ -import Autocomplete from '@mui/material/Autocomplete'; import * as React from 'react'; -import { TextField } from 'src/components/TextField'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { useInfiniteVolumesQuery } from 'src/queries/volumes/volumes'; interface Props { disabled?: boolean; error?: string; + name: string; onBlur: (e: any) => void; onChange: (volumeId: null | number) => void; region?: string; @@ -14,7 +14,7 @@ interface Props { } export const VolumeSelect = (props: Props) => { - const { disabled, error, onBlur, onChange, region, value } = props; + const { disabled, error, name, onBlur, onChange, region, value } = props; const [inputValue, setInputValue] = React.useState(''); @@ -60,6 +60,9 @@ export const VolumeSelect = (props: Props) => { } }, }} + helperText={ + region && "Only volumes in this Linode's region are attachable." + } onChange={(event, value) => { onChange(value?.id ?? -1); setInputValue(''); @@ -71,23 +74,16 @@ export const VolumeSelect = (props: Props) => { setInputValue(''); } }} - renderInput={(params) => ( - - )} disabled={disabled} + errorText={error} + id={name} inputValue={selectedVolume ? selectedVolume.label : inputValue} isOptionEqualToValue={(option) => option.id === selectedVolume?.id} + label="Volume" loading={isLoading} + onBlur={onBlur} options={options ?? []} + placeholder="Select a Volume" value={selectedVolume} /> ); From c828e21b1f9928e487a456403f772ce7828b2bf5 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Thu, 29 Aug 2024 16:27:09 -0400 Subject: [PATCH 06/15] Disable 'Attach Volume' button when linode requires client library update --- .../Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx index b9e0b168281..62ffa076e4e 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx @@ -100,12 +100,13 @@ export const LinodeVolumeAttachForm = (props: Props) => { const { data: volume } = useVolumeQuery(values.volume_id); + const linodeRequiresClientLibraryUpdate = + volume?.encryption === 'enabled' && + Boolean(!linode.capabilities?.includes('blockstorage_encryption')); + React.useEffect(() => { // When the volume is encrypted but the linode requires a client library update, we want to show the client library copy - setClientLibraryCopyVisible( - volume?.encryption === 'enabled' && - Boolean(!linode.capabilities?.includes('blockstorage_encryption')) - ); + setClientLibraryCopyVisible(linodeRequiresClientLibraryUpdate); }, [volume]); return ( @@ -140,7 +141,7 @@ export const LinodeVolumeAttachForm = (props: Props) => { /> Date: Fri, 30 Aug 2024 12:35:30 -0400 Subject: [PATCH 07/15] Passing create-volume.spec.ts --- .../e2e/core/volumes/create-volume.spec.ts | 4 ++-- .../manager/cypress/support/intercepts/volumes.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index c6b402f3177..cb099960a39 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -6,6 +6,7 @@ import { cleanUp } from 'support/util/cleanup'; import { containsClick, fbtVisible, fbtClick, getClick } from 'support/helpers'; import { interceptCreateVolume, + mockGetVolume, mockGetVolumes, } from 'support/intercepts/volumes'; import { randomNumber, randomString, randomLabel } from 'support/util/random'; @@ -195,6 +196,7 @@ describe('volume create flow', () => { (linode: Linode) => { linode.capabilities?.map((capability) => cy.log(capability)); mockGetVolumes([volume]).as('getVolumes'); + mockGetVolume(volume); cy.visitWithLogin(`/linodes/${linode.id}/storage`); cy.wait(['@getFeatureFlags', '@getAccount']); @@ -213,8 +215,6 @@ describe('volume create flow', () => { cy.wait(['@getVolumes']); cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - cy.log(volume.encryption ?? 'vol encryption disabled'); - // Ensure notice is displayed in "Attach Existing Volume" view when an encrypted volume is selected cy.findByPlaceholderText('Select a Volume') .should('be.visible') diff --git a/packages/manager/cypress/support/intercepts/volumes.ts b/packages/manager/cypress/support/intercepts/volumes.ts index e0eb89ca056..90bc5f97669 100644 --- a/packages/manager/cypress/support/intercepts/volumes.ts +++ b/packages/manager/cypress/support/intercepts/volumes.ts @@ -20,6 +20,21 @@ export const mockGetVolumes = (volumes: Volume[]): Cypress.Chainable => { return cy.intercept('GET', apiMatcher('volumes*'), paginateResponse(volumes)); }; +/** + * Intercepts GET request to fetch a Volume and mocks response. + * + * @param volume - Volume with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetVolume = (volume: Volume): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher(`volumes/${volume.id}`), + makeResponse(volume) + ); +}; + /** * Intercepts POST request to create a Volume. * From 174d7a76ac7a21e686f4a1a341051d7e573cc71c Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 30 Aug 2024 12:57:54 -0400 Subject: [PATCH 08/15] Added changeset: Change 'bs_encryption_supported' property on Linode object to 'capabilities' --- .../.changeset/pr-10837-upcoming-features-1725037074546.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-10837-upcoming-features-1725037074546.md diff --git a/packages/api-v4/.changeset/pr-10837-upcoming-features-1725037074546.md b/packages/api-v4/.changeset/pr-10837-upcoming-features-1725037074546.md new file mode 100644 index 00000000000..abc2f4dc66b --- /dev/null +++ b/packages/api-v4/.changeset/pr-10837-upcoming-features-1725037074546.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Change 'bs_encryption_supported' property on Linode object to 'capabilities' ([#10837](https://github.com/linode/manager/pull/10837)) From 77f92325bcb5a3f05247dc865d9585ae813aff00 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 30 Aug 2024 13:02:01 -0400 Subject: [PATCH 09/15] Added changeset: Unit test for LinodeVolumeAddDrawer and E2E test for client library update notices in Create/Attach Volume drawer --- packages/manager/.changeset/pr-10837-tests-1725037321098.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10837-tests-1725037321098.md diff --git a/packages/manager/.changeset/pr-10837-tests-1725037321098.md b/packages/manager/.changeset/pr-10837-tests-1725037321098.md new file mode 100644 index 00000000000..0ee9e596276 --- /dev/null +++ b/packages/manager/.changeset/pr-10837-tests-1725037321098.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Unit test for LinodeVolumeAddDrawer and E2E test for client library update notices in Create/Attach Volume drawer ([#10837](https://github.com/linode/manager/pull/10837)) From 1fa00291bd65a5b76fc19ac45655137b2eff0b75 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 30 Aug 2024 13:05:09 -0400 Subject: [PATCH 10/15] Added changeset: Support Volume Encryption and associated notices in Create/Attach Volume drawer --- .../.changeset/pr-10837-upcoming-features-1725037508758.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10837-upcoming-features-1725037508758.md diff --git a/packages/manager/.changeset/pr-10837-upcoming-features-1725037508758.md b/packages/manager/.changeset/pr-10837-upcoming-features-1725037508758.md new file mode 100644 index 00000000000..63f9544bee7 --- /dev/null +++ b/packages/manager/.changeset/pr-10837-upcoming-features-1725037508758.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Support Volume Encryption and associated notices in Create/Attach Volume drawer ([#10837](https://github.com/linode/manager/pull/10837)) From 3d71087569c84d64762b27253222632fdcfc0e95 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 30 Aug 2024 14:32:47 -0400 Subject: [PATCH 11/15] Update E2E test to reflect 'Create Volume' --> 'Add Volume' change --- .../manager/cypress/e2e/core/volumes/create-volume.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index cabb6e7e87a..d564326285d 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -202,8 +202,8 @@ describe('volume create flow', () => { cy.visitWithLogin(`/linodes/${linode.id}/storage`); cy.wait(['@getFeatureFlags', '@getAccount']); - // Click "Create Volume" button - cy.findByText('Create Volume').click(); + // Click "Add Volume" button + cy.findByText('Add Volume').click(); cy.get('[data-qa-drawer="true"]').within(() => { cy.get('[data-qa-checked]').should('be.visible').click(); From b8dd4c31bb75f49d7fe85ae9c69b012bc68621a8 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Thu, 5 Sep 2024 13:19:53 -0400 Subject: [PATCH 12/15] Add invalidations for individual volume query & refactor useDetachVolumeMutation --- .../features/Volumes/DetachVolumeDialog.tsx | 4 +-- .../manager/src/queries/volumes/volumes.ts | 30 +++++++++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx index 4e1d7d6e186..51f1bdf39af 100644 --- a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx @@ -32,10 +32,10 @@ export const DetachVolumeDialog = (props: Props) => { error, isPending, mutateAsync: detachVolume, - } = useDetachVolumeMutation(); + } = useDetachVolumeMutation(volume?.id ?? -1); const onDetach = () => { - detachVolume({ id: volume?.id ?? -1 }).then(() => { + detachVolume().then(() => { onClose(); checkForNewEvents(); enqueueSnackbar(`Volume detachment started`, { diff --git a/packages/manager/src/queries/volumes/volumes.ts b/packages/manager/src/queries/volumes/volumes.ts index aedf8d51f7d..02bcf5aa7f8 100644 --- a/packages/manager/src/queries/volumes/volumes.ts +++ b/packages/manager/src/queries/volumes/volumes.ts @@ -136,6 +136,11 @@ export const useResizeVolumeMutation = () => { return useMutation({ mutationFn: ({ volumeId, ...data }) => resizeVolume(volumeId, data), onSuccess(volume) { + // Invalidate the specific volume + queryClient.invalidateQueries({ + queryKey: volumeQueries.volume(volume.id).queryKey, + }); + // Invalidate all lists queryClient.invalidateQueries({ queryKey: volumeQueries.lists.queryKey, @@ -227,6 +232,11 @@ export const useUpdateVolumeMutation = () => { return useMutation({ mutationFn: ({ volumeId, ...data }) => updateVolume(volumeId, data), onSuccess(volume) { + // Invalidate the specific volume + queryClient.invalidateQueries({ + queryKey: volumeQueries.volume(volume.id).queryKey, + }); + // Invalidate all lists queryClient.invalidateQueries({ queryKey: volumeQueries.lists.queryKey, @@ -250,10 +260,16 @@ export const useAttachVolumeMutation = () => { return useMutation({ mutationFn: ({ volumeId, ...data }) => attachVolume(volumeId, data), onSuccess(volume) { + // Invalidate the specific volume + queryClient.invalidateQueries({ + queryKey: volumeQueries.volume(volume.id).queryKey, + }); + // Invalidate all lists queryClient.invalidateQueries({ queryKey: volumeQueries.lists.queryKey, }); + // If the volume is assigned to a Linode, invalidate that Linode's list if (volume.linode_id) { queryClient.invalidateQueries({ @@ -264,7 +280,15 @@ export const useAttachVolumeMutation = () => { }); }; -export const useDetachVolumeMutation = () => - useMutation<{}, APIError[], { id: number }>({ - mutationFn: ({ id }) => detachVolume(id), +export const useDetachVolumeMutation = (volumeId: number) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[]>({ + mutationFn: () => detachVolume(volumeId), + onSuccess() { + // Invalidate the volume + queryClient.invalidateQueries({ + queryKey: volumeQueries.volume(volumeId).queryKey, + }); + }, }); +}; From 4d0247103c9260abb157b4a83d9feddef3f1611d Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 6 Sep 2024 11:29:28 -0400 Subject: [PATCH 13/15] Revert useDetachVolumeMutation changes; use setQueryData in relevant Volumes queries --- .../features/Volumes/DetachVolumeDialog.tsx | 4 +- .../manager/src/queries/volumes/volumes.ts | 43 ++++++++----------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx index 51f1bdf39af..4e1d7d6e186 100644 --- a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx @@ -32,10 +32,10 @@ export const DetachVolumeDialog = (props: Props) => { error, isPending, mutateAsync: detachVolume, - } = useDetachVolumeMutation(volume?.id ?? -1); + } = useDetachVolumeMutation(); const onDetach = () => { - detachVolume().then(() => { + detachVolume({ id: volume?.id ?? -1 }).then(() => { onClose(); checkForNewEvents(); enqueueSnackbar(`Volume detachment started`, { diff --git a/packages/manager/src/queries/volumes/volumes.ts b/packages/manager/src/queries/volumes/volumes.ts index 02bcf5aa7f8..7c0add54f1f 100644 --- a/packages/manager/src/queries/volumes/volumes.ts +++ b/packages/manager/src/queries/volumes/volumes.ts @@ -136,11 +136,11 @@ export const useResizeVolumeMutation = () => { return useMutation({ mutationFn: ({ volumeId, ...data }) => resizeVolume(volumeId, data), onSuccess(volume) { - // Invalidate the specific volume - queryClient.invalidateQueries({ - queryKey: volumeQueries.volume(volume.id).queryKey, - }); - + // Update the specific volume + queryClient.setQueryData( + volumeQueries.volume(volume.id).queryKey, + volume + ); // Invalidate all lists queryClient.invalidateQueries({ queryKey: volumeQueries.lists.queryKey, @@ -232,11 +232,11 @@ export const useUpdateVolumeMutation = () => { return useMutation({ mutationFn: ({ volumeId, ...data }) => updateVolume(volumeId, data), onSuccess(volume) { - // Invalidate the specific volume - queryClient.invalidateQueries({ - queryKey: volumeQueries.volume(volume.id).queryKey, - }); - + // Update the specific volume + queryClient.setQueryData( + volumeQueries.volume(volume.id).queryKey, + volume + ); // Invalidate all lists queryClient.invalidateQueries({ queryKey: volumeQueries.lists.queryKey, @@ -260,11 +260,11 @@ export const useAttachVolumeMutation = () => { return useMutation({ mutationFn: ({ volumeId, ...data }) => attachVolume(volumeId, data), onSuccess(volume) { - // Invalidate the specific volume - queryClient.invalidateQueries({ - queryKey: volumeQueries.volume(volume.id).queryKey, - }); - + // Update the specific volume + queryClient.setQueryData( + volumeQueries.volume(volume.id).queryKey, + volume + ); // Invalidate all lists queryClient.invalidateQueries({ queryKey: volumeQueries.lists.queryKey, @@ -280,15 +280,8 @@ export const useAttachVolumeMutation = () => { }); }; -export const useDetachVolumeMutation = (volumeId: number) => { - const queryClient = useQueryClient(); - return useMutation<{}, APIError[]>({ - mutationFn: () => detachVolume(volumeId), - onSuccess() { - // Invalidate the volume - queryClient.invalidateQueries({ - queryKey: volumeQueries.volume(volumeId).queryKey, - }); - }, +export const useDetachVolumeMutation = () => { + return useMutation<{}, APIError[], { id: number }>({ + mutationFn: ({ id }) => detachVolume(id), }); }; From 8012b0c722136617bd3da1210a0ab7da5c3c819c Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 6 Sep 2024 15:55:48 -0400 Subject: [PATCH 14/15] Remove stray leftover cy.log() --- packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index d564326285d..6dfd3bc07dd 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -195,7 +195,6 @@ describe('volume create flow', () => { cy.defer(() => createTestLinode(linodeRequest), 'creating Linode').then( (linode: Linode) => { - linode.capabilities?.map((capability) => cy.log(capability)); mockGetVolumes([volume]).as('getVolumes'); mockGetVolume(volume); From 3126161e5cd4add632a0a7b20115cf9b844c6fb7 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 6 Sep 2024 17:03:29 -0400 Subject: [PATCH 15/15] Specific volume invalidation in volumeEventsHandler --- packages/manager/src/queries/volumes/events.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/manager/src/queries/volumes/events.ts b/packages/manager/src/queries/volumes/events.ts index 3f2415f0891..41da1d100b0 100644 --- a/packages/manager/src/queries/volumes/events.ts +++ b/packages/manager/src/queries/volumes/events.ts @@ -16,6 +16,12 @@ export const volumeEventsHandler = ({ invalidateQueries({ queryKey: volumeQueries.lists.queryKey, }); + + if (event.entity) { + invalidateQueries({ + queryKey: volumeQueries.volume(event.entity.id).queryKey, + }); + } } if (