Skip to content

Commit

Permalink
change: [M3-6546] - Volume drawers and action menu refactored
Browse files Browse the repository at this point in the history
  • Loading branch information
dchyrva-akamai committed Dec 16, 2024
1 parent c8fa6b3 commit 2133015
Show file tree
Hide file tree
Showing 36 changed files with 363 additions and 108 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10820-changed-1725610034084.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@linode/manager': Changed
---

Volume drawers and action menu ([#10820](https://github.com/linode/manager/pull/10820))
18 changes: 1 addition & 17 deletions packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import type { VolumeRequestPayload } from '@linode/api-v4';
import { createVolume } from '@linode/api-v4/lib/volumes';
import { Volume } from '@linode/api-v4';
import { volumeRequestPayloadFactory } from 'src/factories/volume';
import { authenticate } from 'support/api/authentication';
import { interceptCloneVolume } from 'support/intercepts/volumes';
import { SimpleBackoffMethod } from 'support/util/backoff';
import { cleanUp } from 'support/util/cleanup';
import { pollVolumeStatus } from 'support/util/polling';
import { randomLabel } from 'support/util/random';
import { chooseRegion } from 'support/util/regions';
import { createActiveVolume } from 'support/api/volumes';

// 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.
Expand All @@ -17,19 +14,6 @@ const pageSizeOverride = {
PAGE_SIZE: 100,
};

/**
* Creates a Volume and waits for it to become active.
*
* @param volumeRequest - Volume create request payload.
*
* @returns Promise that resolves to created Volume.
*/
const createActiveVolume = async (volumeRequest: VolumeRequestPayload) => {
const volume = await createVolume(volumeRequest);
await pollVolumeStatus(volume.id, 'active', new SimpleBackoffMethod(10000));
return volume;
};

authenticate();
describe('volume clone flow', () => {
before(() => {
Expand Down
103 changes: 87 additions & 16 deletions packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createVolume } from '@linode/api-v4/lib/volumes';
import { Volume } from '@linode/api-v4';

import { volumeRequestPayloadFactory } from 'src/factories/volume';
import { authenticate } from 'support/api/authentication';
import { randomLabel } from 'support/util/random';
import { chooseRegion } from 'support/util/regions';
import { cleanUp } from 'support/util/cleanup';
import { ui } from 'support/ui';
import { createActiveVolume } from 'support/api/volumes';

authenticate();
describe('volume update flow', () => {
Expand All @@ -16,18 +18,17 @@ describe('volume update flow', () => {
});

/*
* - Confirms that volume label and tags can be changed from the Volumes landing page.
* - Confirms that volume label can be changed from the Volumes landing page.
*/
it("updates a volume's label and tags", () => {
it("updates a volume's label", () => {
const volumeRequest = volumeRequestPayloadFactory.build({
label: randomLabel(),
region: chooseRegion().id,
});

const newLabel = randomLabel();
const newTags = [randomLabel(5), randomLabel(5), randomLabel(5)];

cy.defer(() => createVolume(volumeRequest), 'creating volume').then(
cy.defer(() => createActiveVolume(volumeRequest), 'creating volume').then(
(volume: Volume) => {
cy.visitWithLogin('/volumes', {
// Temporarily force volume table to show up to 100 results per page.
Expand All @@ -43,17 +44,83 @@ describe('volume update flow', () => {
.should('be.visible')
.closest('tr')
.within(() => {
cy.findByText('Edit').click();
cy.findByText('active').should('be.visible');
});
ui.actionMenu
.findByTitle(`Action menu for Volume ${volume.label}`)
.should('be.visible')
.click();
cy.get('[data-testid="Edit"]').click();

// Enter new label and add tags, click "Save Changes".
// Enter new label, click "Save Changes".
cy.get('[data-qa-drawer="true"]').within(() => {
cy.findByText('Edit Volume').should('be.visible');
cy.findByDisplayValue(volume.label)
.should('be.visible')
.click()
.type(`{selectall}{backspace}${newLabel}`);

cy.findByText('Save Changes').should('be.visible').click();
});

// Confirm new label is applied, click "Edit" to re-open drawer.
cy.findByText(newLabel).should('be.visible');
ui.actionMenu
.findByTitle(`Action menu for Volume ${newLabel}`)
.should('be.visible')
.click();
cy.get('[data-testid="Edit"]').click();

// Confirm new label is shown.
cy.get('[data-qa-drawer="true"]').within(() => {
cy.findByText('Edit Volume').should('be.visible');
cy.findByDisplayValue(newLabel).should('be.visible');
});
}
);
});

/*
* - Confirms that volume tags can be changed from the Volumes landing page.
*/
it("updates volume's tags", () => {
const volumeRequest = volumeRequestPayloadFactory.build({
label: randomLabel(),
region: chooseRegion().id,
});

const newTags = [randomLabel(5), randomLabel(5), randomLabel(5)];

cy.defer(() => createActiveVolume(volumeRequest), 'creating volume').then(
(volume: Volume) => {
cy.visitWithLogin('/volumes', {
// Temporarily force volume table to show up to 100 results per page.
// This is a workaround while we wait to get stuck volumes removed.
// @TODO Remove local storage override when stuck volumes are removed from test accounts.
localStorageOverrides: {
PAGE_SIZE: 100,
},
});

// Confirm that volume is listed on landing page, click "Edit" to open drawer.
cy.findByText(volume.label)
.should('be.visible')
.closest('tr')
.within(() => {
cy.findByText('active').should('be.visible');
});

ui.actionMenu
.findByTitle(`Action menu for Volume ${volume.label}`)
.should('be.visible')
.click();

cy.get('[data-testid="Manage Tags"]').click();

// Add tags, click "Save Changes".
cy.get('[data-qa-drawer="true"]').within(() => {
cy.findByText('Manage Volume Tags').should('be.visible');

cy.findByPlaceholderText('Type to choose or create a tag.')
.should('be.visible')
.click()
Expand All @@ -62,18 +129,18 @@ describe('volume update flow', () => {
cy.findByText('Save Changes').should('be.visible').click();
});

// Confirm new label is applied, click "Edit" to re-open drawer.
cy.findByText(newLabel)
// Confirm new tags are shown, click "Manage Volume Tags" to re-open drawer.
cy.findByText(volumeRequest.label).should('be.visible');

ui.actionMenu
.findByTitle(`Action menu for Volume ${volume.label}`)
.should('be.visible')
.closest('tr')
.within(() => {
cy.findByText('Edit').click();
});
.click();

cy.get('[data-testid="Manage Tags"]').click();

// Confirm new label and tags are shown.
cy.get('[data-qa-drawer="true"]').within(() => {
cy.findByText('Edit Volume').should('be.visible');
cy.findByDisplayValue(newLabel).should('be.visible');
cy.findByText('Manage Volume Tags').should('be.visible');

// Click the tags input field to see all the selected tags
cy.findByRole('combobox').should('be.visible').click();
Expand All @@ -85,4 +152,8 @@ describe('volume update flow', () => {
}
);
});

after(() => {
cleanUp(['tags', 'volumes']);
});
});
28 changes: 26 additions & 2 deletions packages/manager/cypress/support/api/volumes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { Volume, deleteVolume, detachVolume, getVolumes } from '@linode/api-v4';
import {
createVolume,
deleteVolume,
detachVolume,
getVolumes,
} from '@linode/api-v4';
import { pageSize } from 'support/constants/api';
import { SimpleBackoffMethod, attemptWithBackoff } from 'support/util/backoff';
import { depaginate } from 'support/util/paginate';
import { pollVolumeStatus } from 'support/util/polling';

import { isTestLabel } from './common';
import { attemptWithBackoff, SimpleBackoffMethod } from 'support/util/backoff';

import type { Volume, VolumeRequestPayload } from '@linode/api-v4';

/**
* Delete all Volumes whose labels are prefixed "cy-test-".
Expand Down Expand Up @@ -45,3 +54,18 @@ export const deleteAllTestVolumes = async (): Promise<void> => {

await Promise.all(detachDeletePromises);
};

/**
* Creates a Volume and waits for it to become active.
*
* @param volumeRequest - Volume create request payload.
*
* @returns Promise that resolves to created Volume.
*/
export const createActiveVolume = async (
volumeRequest: VolumeRequestPayload
) => {
const volume = await createVolume(volumeRequest);
await pollVolumeStatus(volume.id, 'active', new SimpleBackoffMethod(10000));
return volume;
};
1 change: 1 addition & 0 deletions packages/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"build:analyze": "bunx vite-bundle-visualizer",
"precommit": "lint-staged && yarn typecheck",
"test": "vitest run",
"test:ui": "vitest --ui",
"test:debug": "node --inspect-brk scripts/test.js --runInBand",
"storybook": "NODE_OPTIONS='--max-old-space-size=4096' storybook dev -p 6006",
"storybook-static": "storybook build -c .storybook -o .out",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
import { TableRowError } from 'src/components/TableRowError/TableRowError';
import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading';
import { TableSortCell } from 'src/components/TableSortCell';
import { CloneVolumeDrawer } from 'src/features/Volumes/CloneVolumeDrawer';
import { DeleteVolumeDialog } from 'src/features/Volumes/DeleteVolumeDialog';
import { DetachVolumeDialog } from 'src/features/Volumes/DetachVolumeDialog';
import { EditVolumeDrawer } from 'src/features/Volumes/EditVolumeDrawer';
import { ResizeVolumeDrawer } from 'src/features/Volumes/ResizeVolumeDrawer';
import { VolumeDetailsDrawer } from 'src/features/Volumes/VolumeDetailsDrawer';
import { LinodeVolumeAddDrawer } from 'src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer';
import { DeleteVolumeDialog } from 'src/features/Volumes/Dialogs/DeleteVolumeDialog';
import { DetachVolumeDialog } from 'src/features/Volumes/Dialogs/DetachVolumeDialog';
import { CloneVolumeDrawer } from 'src/features/Volumes/Drawers/CloneVolumeDrawer';
import { EditVolumeDrawer } from 'src/features/Volumes/Drawers/EditVolumeDrawer';
import { ManageTagsDrawer } from 'src/features/Volumes/Drawers/ManageTagsDrawer';
import { ResizeVolumeDrawer } from 'src/features/Volumes/Drawers/ResizeVolumeDrawer';
import { VolumeDetailsDrawer } from 'src/features/Volumes/Drawers/VolumeDetailsDrawer';
import { LinodeVolumeAddDrawer } from 'src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer';
import { VolumeTableRow } from 'src/features/Volumes/VolumeTableRow';
import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted';
import { useOrder } from 'src/hooks/useOrder';
Expand Down Expand Up @@ -75,6 +76,9 @@ export const LinodeVolumes = () => {
isBlockStorageEncryptionFeatureEnabled,
} = useIsBlockStorageEncryptionFeatureEnabled();

const [isManageTagsDrawerOpen, setisManageTagsDrawerOpen] = React.useState(
false
);
const [selectedVolumeId, setSelectedVolumeId] = React.useState<number>();
const [isDetailsDrawerOpen, setIsDetailsDrawerOpen] = React.useState(false);
const [isEditDrawerOpen, setIsEditDrawerOpen] = React.useState(false);
Expand Down Expand Up @@ -106,6 +110,11 @@ export const LinodeVolumes = () => {
setIsEditDrawerOpen(true);
};

const handleManageTags = (volume: Volume) => {
setSelectedVolumeId(volume.id);
setisManageTagsDrawerOpen(true);
};

const handleResize = (volume: Volume) => {
setSelectedVolumeId(volume.id);
setIsResizeDrawerOpen(true);
Expand Down Expand Up @@ -156,6 +165,7 @@ export const LinodeVolumes = () => {
handleDetach: () => handleDetach(volume),
handleDetails: () => handleDetails(volume),
handleEdit: () => handleEdit(volume),
handleManageTags: () => handleManageTags(volume),
handleResize: () => handleResize(volume),
handleUpgrade: () => null,
}}
Expand Down Expand Up @@ -258,6 +268,11 @@ export const LinodeVolumes = () => {
open={isEditDrawerOpen}
volume={selectedVolume}
/>
<ManageTagsDrawer
onClose={() => setisManageTagsDrawerOpen(false)}
open={isManageTagsDrawerOpen}
volume={selectedVolume}
/>
<ResizeVolumeDrawer
onClose={() => setIsResizeDrawerOpen(false)}
open={isResizeDrawerOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useSnackbar } from 'notistack';
import * as React from 'react';

import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
import { VolumeUpgradeCopy } from 'src/features/Volumes/UpgradeVolumeDialog';
import { VolumeUpgradeCopy } from 'src/features/Volumes/Dialogs/UpgradeVolumeDialog';
import { getUpgradeableVolumeIds } from 'src/features/Volumes/utils';
import { useNotificationsQuery } from 'src/queries/account/notifications';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants
import { PricePanel } from './VolumeDrawer/PricePanel';

import type { Volume } from '@linode/api-v4';

interface Props {
isFetching?: boolean;
onClose: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Drawer } from 'src/components/Drawer';
import { BLOCK_STORAGE_ENCRYPTION_SETTING_IMMUTABLE_COPY } from 'src/components/Encryption/constants';
import { useIsBlockStorageEncryptionFeatureEnabled } from 'src/components/Encryption/utils';
import { TagsInput } from 'src/components/TagsInput/TagsInput';
import { useGrants } from 'src/queries/profile/profile';
import { useUpdateVolumeMutation } from 'src/queries/volumes/volumes';
import {
Expand Down Expand Up @@ -48,17 +47,15 @@ export const EditVolumeDrawer = (props: Props) => {
handleSubmit,
isSubmitting,
resetForm,
setFieldValue,
status: error,
touched,
values,
} = useFormik({
enableReinitialize: true,
initialValues: { label: volume?.label ?? '', tags: volume?.tags ?? [] },
async onSubmit(values, { setErrors, setStatus }) {
try {
await updateVolume({
label: values.label ?? '',
label: values.label,
tags: values.tags,
volumeId: volume?.id ?? -1,
});
Expand Down Expand Up @@ -97,6 +94,7 @@ export const EditVolumeDrawer = (props: Props) => {
/>
)}
{error && <Notice text={error} variant="error" />}

<TextField
disabled={isReadOnly}
errorText={errors.label}
Expand All @@ -107,25 +105,7 @@ export const EditVolumeDrawer = (props: Props) => {
required
value={values.label}
/>
<TagsInput
onChange={(selected) =>
setFieldValue(
'tags',
selected.map((item) => item.value)
)
}
tagError={
touched.tags
? errors.tags
? 'Unable to tag volume.'
: undefined
: undefined
}
disabled={isReadOnly}
label="Tags"
name="tags"
value={values.tags?.map((t) => ({ label: t, value: t })) ?? []}
/>

{isBlockStorageEncryptionFeatureEnabled && (
<Box
sx={{
Expand All @@ -141,6 +121,7 @@ export const EditVolumeDrawer = (props: Props) => {
/>
</Box>
)}

<ActionsPanel
primaryButtonProps={{
disabled: isReadOnly || !dirty,
Expand Down
Loading

0 comments on commit 2133015

Please sign in to comment.