diff --git a/packages/manager/.changeset/pr-10820-changed-1725610034084.md b/packages/manager/.changeset/pr-10820-changed-1725610034084.md new file mode 100644 index 00000000000..9c33175af12 --- /dev/null +++ b/packages/manager/.changeset/pr-10820-changed-1725610034084.md @@ -0,0 +1,5 @@ +--- +'@linode/manager': Changed +--- + +Volume drawers and action menu ([#10820](https://github.com/linode/manager/pull/10820)) diff --git a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts index e65a582243b..146233cae6c 100644 --- a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts @@ -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. @@ -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(() => { diff --git a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts index 70f4840bbad..25b5e128405 100644 --- a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts @@ -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', () => { @@ -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. @@ -43,10 +44,15 @@ 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) @@ -54,6 +60,67 @@ describe('volume update flow', () => { .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() @@ -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(); @@ -85,4 +152,8 @@ describe('volume update flow', () => { } ); }); + + after(() => { + cleanUp(['tags', 'volumes']); + }); }); diff --git a/packages/manager/cypress/support/api/volumes.ts b/packages/manager/cypress/support/api/volumes.ts index a52e4784f13..0ef03c9d816 100644 --- a/packages/manager/cypress/support/api/volumes.ts +++ b/packages/manager/cypress/support/api/volumes.ts @@ -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-". @@ -45,3 +54,18 @@ export const deleteAllTestVolumes = async (): Promise => { 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; +}; diff --git a/packages/manager/package.json b/packages/manager/package.json index 60760e6d0fb..8588c16bec8 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -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", diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeVolumes.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeVolumes.tsx index d24cac6b32d..22c985ebdd0 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeVolumes.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeVolumes.tsx @@ -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'; @@ -75,6 +76,9 @@ export const LinodeVolumes = () => { isBlockStorageEncryptionFeatureEnabled, } = useIsBlockStorageEncryptionFeatureEnabled(); + const [isManageTagsDrawerOpen, setisManageTagsDrawerOpen] = React.useState( + false + ); const [selectedVolumeId, setSelectedVolumeId] = React.useState(); const [isDetailsDrawerOpen, setIsDetailsDrawerOpen] = React.useState(false); const [isEditDrawerOpen, setIsEditDrawerOpen] = React.useState(false); @@ -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); @@ -156,6 +165,7 @@ export const LinodeVolumes = () => { handleDetach: () => handleDetach(volume), handleDetails: () => handleDetails(volume), handleEdit: () => handleEdit(volume), + handleManageTags: () => handleManageTags(volume), handleResize: () => handleResize(volume), handleUpgrade: () => null, }} @@ -258,6 +268,11 @@ export const LinodeVolumes = () => { open={isEditDrawerOpen} volume={selectedVolume} /> + setisManageTagsDrawerOpen(false)} + open={isManageTagsDrawerOpen} + volume={selectedVolume} + /> setIsResizeDrawerOpen(false)} open={isResizeDrawerOpen} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/UpgradeVolumesDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/UpgradeVolumesDialog.tsx index 83204307066..589106b7c63 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/UpgradeVolumesDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/UpgradeVolumesDialog.tsx @@ -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 { diff --git a/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx b/packages/manager/src/features/Volumes/Dialogs/DeleteVolumeDialog.tsx similarity index 100% rename from packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx rename to packages/manager/src/features/Volumes/Dialogs/DeleteVolumeDialog.tsx diff --git a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx b/packages/manager/src/features/Volumes/Dialogs/DetachVolumeDialog.tsx similarity index 100% rename from packages/manager/src/features/Volumes/DetachVolumeDialog.tsx rename to packages/manager/src/features/Volumes/Dialogs/DetachVolumeDialog.tsx diff --git a/packages/manager/src/features/Volumes/UpgradeVolumeDialog.tsx b/packages/manager/src/features/Volumes/Dialogs/UpgradeVolumeDialog.tsx similarity index 100% rename from packages/manager/src/features/Volumes/UpgradeVolumeDialog.tsx rename to packages/manager/src/features/Volumes/Dialogs/UpgradeVolumeDialog.tsx diff --git a/packages/manager/src/features/Volumes/AttachVolumeDrawer.test.tsx b/packages/manager/src/features/Volumes/Drawers/AttachVolumeDrawer.test.tsx similarity index 100% rename from packages/manager/src/features/Volumes/AttachVolumeDrawer.test.tsx rename to packages/manager/src/features/Volumes/Drawers/AttachVolumeDrawer.test.tsx diff --git a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx b/packages/manager/src/features/Volumes/Drawers/AttachVolumeDrawer.tsx similarity index 100% rename from packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx rename to packages/manager/src/features/Volumes/Drawers/AttachVolumeDrawer.tsx diff --git a/packages/manager/src/features/Volumes/CloneVolumeDrawer.test.tsx b/packages/manager/src/features/Volumes/Drawers/CloneVolumeDrawer.test.tsx similarity index 100% rename from packages/manager/src/features/Volumes/CloneVolumeDrawer.test.tsx rename to packages/manager/src/features/Volumes/Drawers/CloneVolumeDrawer.test.tsx diff --git a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx b/packages/manager/src/features/Volumes/Drawers/CloneVolumeDrawer.tsx similarity index 99% rename from packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx rename to packages/manager/src/features/Volumes/Drawers/CloneVolumeDrawer.tsx index 229cb5feafe..097f52abea0 100644 --- a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/Drawers/CloneVolumeDrawer.tsx @@ -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; diff --git a/packages/manager/src/features/Volumes/EditVolumeDrawer.test.tsx b/packages/manager/src/features/Volumes/Drawers/EditVolumeDrawer.test.tsx similarity index 100% rename from packages/manager/src/features/Volumes/EditVolumeDrawer.test.tsx rename to packages/manager/src/features/Volumes/Drawers/EditVolumeDrawer.test.tsx diff --git a/packages/manager/src/features/Volumes/EditVolumeDrawer.tsx b/packages/manager/src/features/Volumes/Drawers/EditVolumeDrawer.tsx similarity index 84% rename from packages/manager/src/features/Volumes/EditVolumeDrawer.tsx rename to packages/manager/src/features/Volumes/Drawers/EditVolumeDrawer.tsx index 02ced6ec1ab..be2ab9d3313 100644 --- a/packages/manager/src/features/Volumes/EditVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/Drawers/EditVolumeDrawer.tsx @@ -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 { @@ -48,9 +47,7 @@ export const EditVolumeDrawer = (props: Props) => { handleSubmit, isSubmitting, resetForm, - setFieldValue, status: error, - touched, values, } = useFormik({ enableReinitialize: true, @@ -58,7 +55,7 @@ export const EditVolumeDrawer = (props: Props) => { async onSubmit(values, { setErrors, setStatus }) { try { await updateVolume({ - label: values.label ?? '', + label: values.label, tags: values.tags, volumeId: volume?.id ?? -1, }); @@ -97,6 +94,7 @@ export const EditVolumeDrawer = (props: Props) => { /> )} {error && } + { required value={values.label} /> - - 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 && ( { /> )} + { + it('should render tags', async () => { + const volume = volumeFactory.build({ + tags: testTags, + }); + + server.use( + http.get(accountEndpoint, () => { + return HttpResponse.json(accountFactory.build()); + }) + ); + + const { getByText } = await renderWithThemeAndRouter( + + ); + + testTags.forEach((tag) => { + expect(getByText(tag)).toBeVisible(); + }); + }); + + it('should disable submit button when form is not dirty', async () => { + const volume = volumeFactory.build(); + + server.use( + http.get(accountEndpoint, () => { + return HttpResponse.json(accountFactory.build()); + }) + ); + + const { getByText } = await renderWithThemeAndRouter( + + ); + + expect(getByText('Save Changes').closest('button')).toBeDisabled(); + }); +}); diff --git a/packages/manager/src/features/Volumes/Drawers/ManageTagsDrawer.tsx b/packages/manager/src/features/Volumes/Drawers/ManageTagsDrawer.tsx new file mode 100644 index 00000000000..0d823ff038a --- /dev/null +++ b/packages/manager/src/features/Volumes/Drawers/ManageTagsDrawer.tsx @@ -0,0 +1,121 @@ +import { Notice } from '@linode/ui'; +import React from 'react'; +import { Controller, useForm } from 'react-hook-form'; + +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Drawer } from 'src/components/Drawer'; +import { TagsInput } from 'src/components/TagsInput/TagsInput'; +import { useGrants } from 'src/queries/profile/profile'; +import { useUpdateVolumeMutation } from 'src/queries/volumes/volumes'; + +import type { APIError, Volume } from '@linode/api-v4'; + +interface Props { + isFetching?: boolean; + onClose: () => void; + open: boolean; + volume: Volume | undefined; +} + +export const ManageTagsDrawer = (props: Props) => { + const { isFetching, onClose: _onClose, open, volume } = props; + + const { data: grants } = useGrants(); + + const { mutateAsync: updateVolume } = useUpdateVolumeMutation(); + + const isReadOnly = + grants !== undefined && + grants.volume.find((grant) => grant.id === volume?.id)?.permissions === + 'read_only'; + + const { + control, + formState: { errors, isDirty, isSubmitting }, + handleSubmit, + reset, + setError, + } = useForm<{ tags: string[] }>({ + values: { tags: volume?.tags ?? [] }, + }); + + const onSubmit = handleSubmit(async (values) => { + try { + await updateVolume({ + label: volume?.label ?? '', + tags: values.tags, + volumeId: volume?.id ?? -1, + }); + + onClose(); + } catch (errors) { + errors.forEach((error: APIError) => { + if (error.field == 'tags') { + setError('tags', { + message: error.reason, + }); + } else { + setError('root', { + message: + 'Unable to edit this Volume at this time. Please try again later.', + }); + } + }); + } + }); + + const onClose = () => { + _onClose(); + reset({ tags: volume?.tags }); + }; + + return ( + +
+ {isReadOnly && ( + + )} + {errors?.root && } + + ( + + field.onChange(selected.map((item) => item.value)) + } + disabled={isReadOnly} + label="Tags" + name="tags" + tagError={fieldState.error?.message} + value={field.value.map((t) => ({ label: t, value: t })) ?? []} + /> + )} + control={control} + name="tags" + /> + + + +
+ ); +}; diff --git a/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx b/packages/manager/src/features/Volumes/Drawers/ResizeVolumeDrawer.tsx similarity index 100% rename from packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx rename to packages/manager/src/features/Volumes/Drawers/ResizeVolumeDrawer.tsx diff --git a/packages/manager/src/features/Volumes/VolumeDetailsDrawer.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDetailsDrawer.tsx similarity index 100% rename from packages/manager/src/features/Volumes/VolumeDetailsDrawer.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDetailsDrawer.tsx diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/ConfigSelect.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/ConfigSelect.tsx similarity index 100% rename from packages/manager/src/features/Volumes/VolumeDrawer/ConfigSelect.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDrawer/ConfigSelect.tsx diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx similarity index 100% rename from packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer.tsx similarity index 100% rename from packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAddDrawer.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer.tsx diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAttachForm.tsx similarity index 100% rename from packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAttachForm.tsx diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx similarity index 100% rename from packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/ModeSelection.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/ModeSelection.tsx similarity index 100% rename from packages/manager/src/features/Volumes/VolumeDrawer/ModeSelection.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDrawer/ModeSelection.tsx diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/PricePanel.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/PricePanel.tsx similarity index 100% rename from packages/manager/src/features/Volumes/VolumeDrawer/PricePanel.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDrawer/PricePanel.tsx diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/SizeField.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/SizeField.tsx similarity index 98% rename from packages/manager/src/features/Volumes/VolumeDrawer/SizeField.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDrawer/SizeField.tsx index 136b1cb32dc..85f22da957c 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/SizeField.tsx +++ b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/SizeField.tsx @@ -14,7 +14,7 @@ import { useVolumeTypesQuery } from 'src/queries/volumes/volumes'; import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; import { getDCSpecificPriceByType } from 'src/utilities/pricing/dynamicPricing'; -import { SIZE_FIELD_WIDTH } from '../constants'; +import { SIZE_FIELD_WIDTH } from '../../constants'; import type { Theme } from '@mui/material/styles'; diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/VolumeSelect.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/VolumeSelect.tsx similarity index 100% rename from packages/manager/src/features/Volumes/VolumeDrawer/VolumeSelect.tsx rename to packages/manager/src/features/Volumes/Drawers/VolumeDrawer/VolumeSelect.tsx diff --git a/packages/manager/src/features/Volumes/VolumeCreate.tsx b/packages/manager/src/features/Volumes/VolumeCreate.tsx index aba8857e316..ea21bb0f349 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate.tsx @@ -58,8 +58,8 @@ import { maybeCastToNumber } from 'src/utilities/maybeCastToNumber'; import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants'; import { SIZE_FIELD_WIDTH } from './constants'; -import { ConfigSelect } from './VolumeDrawer/ConfigSelect'; -import { SizeField } from './VolumeDrawer/SizeField'; +import { ConfigSelect } from './Drawers/VolumeDrawer/ConfigSelect'; +import { SizeField } from './Drawers/VolumeDrawer/SizeField'; import type { VolumeEncryption } from '@linode/api-v4'; import type { Linode } from '@linode/api-v4/lib/linodes/types'; diff --git a/packages/manager/src/features/Volumes/VolumeTableRow.test.tsx b/packages/manager/src/features/Volumes/VolumeTableRow.test.tsx index 2c745da6744..f63cfab51a7 100644 --- a/packages/manager/src/features/Volumes/VolumeTableRow.test.tsx +++ b/packages/manager/src/features/Volumes/VolumeTableRow.test.tsx @@ -31,6 +31,7 @@ const handlers: ActionHandlers = { handleDetach: vi.fn(), handleDetails: vi.fn(), handleEdit: vi.fn(), + handleManageTags: vi.fn(), handleResize: vi.fn(), handleUpgrade: vi.fn(), }; diff --git a/packages/manager/src/features/Volumes/VolumesActionMenu.test.tsx b/packages/manager/src/features/Volumes/VolumesActionMenu.test.tsx index 20a1ad44bc6..f33a114f621 100644 --- a/packages/manager/src/features/Volumes/VolumesActionMenu.test.tsx +++ b/packages/manager/src/features/Volumes/VolumesActionMenu.test.tsx @@ -18,6 +18,7 @@ const props: Props = { handleDetach: vi.fn(), handleDetails: vi.fn(), handleEdit: vi.fn(), + handleManageTags: vi.fn(), handleResize: vi.fn(), handleUpgrade: vi.fn(), }, diff --git a/packages/manager/src/features/Volumes/VolumesActionMenu.tsx b/packages/manager/src/features/Volumes/VolumesActionMenu.tsx index 8217745d659..b7898e8d8bf 100644 --- a/packages/manager/src/features/Volumes/VolumesActionMenu.tsx +++ b/packages/manager/src/features/Volumes/VolumesActionMenu.tsx @@ -1,14 +1,12 @@ -import { Volume } from '@linode/api-v4'; -import { Theme, useTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { splitAt } from 'ramda'; import * as React from 'react'; -import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu'; -import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; +import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import { getRestrictedResourceText } from 'src/features/Account/utils'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; +import type { Volume } from '@linode/api-v4'; +import type { Action } from 'src/components/ActionMenu/ActionMenu'; + export interface ActionHandlers { handleAttach: () => void; handleClone: () => void; @@ -16,6 +14,7 @@ export interface ActionHandlers { handleDetach: () => void; handleDetails: () => void; handleEdit: () => void; + handleManageTags: () => void; handleResize: () => void; handleUpgrade: () => void; } @@ -31,9 +30,6 @@ export const VolumesActionMenu = (props: Props) => { const attached = volume.linode_id !== null; - const theme = useTheme(); - const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); - const isVolumeReadOnly = useIsResourceRestricted({ grantLevel: 'read_only', grantType: 'volume', @@ -57,6 +53,11 @@ export const VolumesActionMenu = (props: Props) => { }) : undefined, }, + { + disabled: isVolumeReadOnly, + onClick: handlers.handleManageTags, + title: 'Manage Tags', + }, { disabled: isVolumeReadOnly, onClick: handlers.handleResize, @@ -126,27 +127,10 @@ export const VolumesActionMenu = (props: Props) => { : undefined, }); - const splitActionsArrayIndex = matchesSmDown ? 0 : 2; - const [inlineActions, menuActions] = splitAt(splitActionsArrayIndex, actions); - return ( - <> - {!matchesSmDown && - inlineActions.map((action) => { - return ( - - ); - })} - - + ); }; diff --git a/packages/manager/src/features/Volumes/VolumesLanding.tsx b/packages/manager/src/features/Volumes/VolumesLanding.tsx index aa2a4305d67..c085ed0eabc 100644 --- a/packages/manager/src/features/Volumes/VolumesLanding.tsx +++ b/packages/manager/src/features/Volumes/VolumesLanding.tsx @@ -34,14 +34,15 @@ import { import { VOLUME_TABLE_PREFERENCE_KEY } from 'src/routes/volumes/constants'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { AttachVolumeDrawer } from './AttachVolumeDrawer'; -import { CloneVolumeDrawer } from './CloneVolumeDrawer'; -import { DeleteVolumeDialog } from './DeleteVolumeDialog'; -import { DetachVolumeDialog } from './DetachVolumeDialog'; -import { EditVolumeDrawer } from './EditVolumeDrawer'; -import { ResizeVolumeDrawer } from './ResizeVolumeDrawer'; -import { UpgradeVolumeDialog } from './UpgradeVolumeDialog'; -import { VolumeDetailsDrawer } from './VolumeDetailsDrawer'; +import { DeleteVolumeDialog } from './Dialogs/DeleteVolumeDialog'; +import { DetachVolumeDialog } from './Dialogs/DetachVolumeDialog'; +import { UpgradeVolumeDialog } from './Dialogs/UpgradeVolumeDialog'; +import { AttachVolumeDrawer } from './Drawers/AttachVolumeDrawer'; +import { CloneVolumeDrawer } from './Drawers/CloneVolumeDrawer'; +import { EditVolumeDrawer } from './Drawers/EditVolumeDrawer'; +import { ManageTagsDrawer } from './Drawers/ManageTagsDrawer'; +import { ResizeVolumeDrawer } from './Drawers/ResizeVolumeDrawer'; +import { VolumeDetailsDrawer } from './Drawers/VolumeDetailsDrawer'; import { VolumesLandingEmptyState } from './VolumesLandingEmptyState'; import { VolumeTableRow } from './VolumeTableRow'; @@ -129,6 +130,14 @@ export const VolumesLanding = () => { }); }; + const handleManageTags = (volume: Volume) => { + navigate({ + params: { action: 'manage-tags', volumeId: volume.id }, + search: (prev) => prev, + to: `/volumes/$volumeId/$action`, + }); + }; + const handleEdit = (volume: Volume) => { navigate({ params: { action: 'edit', volumeId: volume.id }, @@ -310,6 +319,7 @@ export const VolumesLanding = () => { handleDetach: () => handleDetach(volume), handleDetails: () => handleDetails(volume), handleEdit: () => handleEdit(volume), + handleManageTags: () => handleManageTags(volume), handleResize: () => handleResize(volume), handleUpgrade: () => handleUpgrade(volume), }} @@ -342,6 +352,12 @@ export const VolumesLanding = () => { open={params.action === 'details'} volume={selectedVolume} /> + }); export const UpdateVolumeSchema = object({ - label: string().required(), + label: string(), + tags: array().of(string()), }); export const AttachVolumeSchema = object({