diff --git a/packages/api-v4/.changeset/pr-12903-added-1759133172067.md b/packages/api-v4/.changeset/pr-12903-added-1759133172067.md new file mode 100644 index 00000000000..9c9010c55aa --- /dev/null +++ b/packages/api-v4/.changeset/pr-12903-added-1759133172067.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +Volume io_ready property ([#12903](https://github.com/linode/manager/pull/12903)) diff --git a/packages/api-v4/src/volumes/types.ts b/packages/api-v4/src/volumes/types.ts index e0d9433d61c..5a469c7cd05 100644 --- a/packages/api-v4/src/volumes/types.ts +++ b/packages/api-v4/src/volumes/types.ts @@ -2,10 +2,21 @@ export type VolumeEncryption = 'disabled' | 'enabled'; export interface Volume { created: string; - encryption?: VolumeEncryption; // @TODO BSE: Remove optionality once BSE is fully rolled out + /** + * Indicates whether a volume is encrypted or not + * + * @TODO BSE: Remove optionality once BSE is fully rolled out + */ + encryption?: VolumeEncryption; // filesystem_path: string; hardware_type: VolumeHardwareType; id: number; + /** + * Indicates whether a volume is ready for I/O operations + * + * @TODO Remove optionality once io_ready is fully rolled out + */ + io_ready?: boolean; label: string; linode_id: null | number; linode_label: null | string; diff --git a/packages/manager/.changeset/pr-12903-added-1759133146190.md b/packages/manager/.changeset/pr-12903-added-1759133146190.md new file mode 100644 index 00000000000..3d24bd15361 --- /dev/null +++ b/packages/manager/.changeset/pr-12903-added-1759133146190.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Volume attached to state ([#12903](https://github.com/linode/manager/pull/12903)) diff --git a/packages/manager/src/features/Volumes/Partials/AttachedToValue.test.tsx b/packages/manager/src/features/Volumes/Partials/AttachedToValue.test.tsx new file mode 100644 index 00000000000..f4ad6a76a7e --- /dev/null +++ b/packages/manager/src/features/Volumes/Partials/AttachedToValue.test.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; + +import { volumeFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { AttachedToValue } from './AttachedToValue'; + +describe('Volume action menu', () => { + it('should show Linode label if the volume is attached', () => { + const volume = volumeFactory.build({ + linode_id: 1, + linode_label: 'linode_1', + io_ready: true, + }); + + const { getByText } = renderWithTheme(); + + expect(getByText(volume.linode_label!)).toBeVisible(); + }); + + it('should show Detach button if Linode is attached and onDetach function is provided', () => { + const volume = volumeFactory.build({ + linode_id: 1, + linode_label: 'linode_1', + io_ready: true, + }); + + const onDetach = () => {}; + + const { getByText } = renderWithTheme( + + ); + + expect(getByText(volume.linode_label!)).toBeVisible(); + expect(getByText('Detach')).toBeVisible(); + }); + + it('should show Linode (restricted) if the Volume is attached to a restricted Linode', () => { + const volume = volumeFactory.build({ + linode_id: 1, + linode_label: null, + io_ready: true, + }); + + const { getByText } = renderWithTheme(); + + expect(getByText('Linode (restricted)')).toBeVisible(); + }); + + it('should show Unattached if the Volume is not attached to a Linode', () => { + const volume = volumeFactory.build({ + linode_id: null, + linode_label: null, + io_ready: false, + }); + + const { getByText } = renderWithTheme(); + + expect(getByText('Unattached')).toBeVisible(); + }); +}); diff --git a/packages/manager/src/features/Volumes/Partials/AttachedToValue.tsx b/packages/manager/src/features/Volumes/Partials/AttachedToValue.tsx new file mode 100644 index 00000000000..d49572ed410 --- /dev/null +++ b/packages/manager/src/features/Volumes/Partials/AttachedToValue.tsx @@ -0,0 +1,53 @@ +import { Box, LinkButton, TooltipIcon, Typography, useTheme } from '@linode/ui'; +import * as React from 'react'; + +import { Link } from 'src/components/Link'; + +import type { Volume } from '@linode/api-v4'; + +interface Props { + onDetach?: () => void; + volume: Volume; +} + +export const AttachedToValue = ({ onDetach, volume }: Props) => { + const theme = useTheme(); + + if (volume.linode_label !== null && volume.linode_id !== null) { + return ( + + + {volume.linode_label} + + + {onDetach && ( + <> + | Detach + + )} + + ); + } + + if (volume.linode_label === null && volume.io_ready) { + return ( + + + Linode (restricted) + + + + ); + } + + return Unattached; +}; diff --git a/packages/manager/src/features/Volumes/Partials/VolumeTableRow.test.tsx b/packages/manager/src/features/Volumes/Partials/VolumeTableRow.test.tsx index 0cf318243ab..770ad7a2fa8 100644 --- a/packages/manager/src/features/Volumes/Partials/VolumeTableRow.test.tsx +++ b/packages/manager/src/features/Volumes/Partials/VolumeTableRow.test.tsx @@ -66,15 +66,15 @@ describe('Volume table row', () => { ); // Check row for basic values - expect(getByText(attachedVolume.label)); - expect(getByText(attachedVolume.size, { exact: false })); - expect(getByTestId('region')); - expect(getByText(attachedVolume.linode_label!)); + expect(getByText(attachedVolume.label)).toBeVisible(); + expect(getByText(attachedVolume.size, { exact: false })).toBeVisible(); + expect(getByTestId('region')).toBeVisible(); + expect(getByText(attachedVolume.linode_label!)).toBeVisible(); await userEvent.click(getByLabelText(/^Action menu for/)); // Make sure there is a detach button - expect(getByText('Detach')); + expect(getByText('Detach')).toBeVisible(); }); it('should show Unattached if the Volume is not attached to a Linode', async () => { @@ -83,12 +83,12 @@ describe('Volume table row', () => { ) ); - expect(getByText('Unattached')); + expect(getByText('Unattached')).toBeVisible(); await userEvent.click(getByLabelText(/^Action menu for/)); // Make sure there is an attach button - expect(getByText('Attach')); + expect(getByText('Attach')).toBeVisible(); }); it('should render an upgrade chip if the volume is eligible for an upgrade', async () => { @@ -173,8 +173,8 @@ describe('Volume table row - for linodes detail page', () => { ); // Check row for basic values - expect(getByText(attachedVolume.label)); - expect(getByText(attachedVolume.size, { exact: false })); + expect(getByText(attachedVolume.label)).toBeVisible(); + expect(getByText(attachedVolume.size, { exact: false })).toBeVisible(); // Because we are on a Linode details page that has the region, we don't need to show // the volume's region. A Volume attached to a Linode must be in the same region. @@ -186,7 +186,7 @@ describe('Volume table row - for linodes detail page', () => { await userEvent.click(getByLabelText(/^Action menu for/)); // Make sure there is a detach button - expect(getByText('Detach')); + expect(getByText('Detach')).toBeVisible(); }); it('should show a high performance icon tooltip if Linode has the capability', async () => { diff --git a/packages/manager/src/features/Volumes/Partials/VolumeTableRow.tsx b/packages/manager/src/features/Volumes/Partials/VolumeTableRow.tsx index 225627608b1..00bbb21a27d 100644 --- a/packages/manager/src/features/Volumes/Partials/VolumeTableRow.tsx +++ b/packages/manager/src/features/Volumes/Partials/VolumeTableRow.tsx @@ -1,5 +1,5 @@ import { useNotificationsQuery, useRegionsQuery } from '@linode/queries'; -import { Box, Chip, Typography } from '@linode/ui'; +import { Box, Chip } from '@linode/ui'; import { Hidden } from '@linode/ui'; import { getFormattedStatus } from '@linode/utilities'; import { useNavigate } from '@tanstack/react-router'; @@ -19,6 +19,7 @@ import { getEventProgress, volumeStatusIconMap, } from '../utils'; +import { AttachedToValue } from './AttachedToValue'; import { VolumesActionMenu } from './VolumesActionMenu'; import type { ActionHandlers } from './VolumesActionMenu'; @@ -193,16 +194,7 @@ export const VolumeTableRow = React.memo((props: Props) => { )} {isVolumesLanding && ( - {volume.linode_id !== null ? ( - - {volume.linode_label} - - ) : ( - Unattached - )} + )} {isBlockStorageEncryptionFeatureEnabled && ( diff --git a/packages/manager/src/features/Volumes/VolumeDetails/VolumeEntityDetails/VolumeEntityDetailBody.tsx b/packages/manager/src/features/Volumes/VolumeDetails/VolumeEntityDetails/VolumeEntityDetailBody.tsx index bd6bdec9eb2..656f239ad71 100644 --- a/packages/manager/src/features/Volumes/VolumeDetails/VolumeEntityDetails/VolumeEntityDetailBody.tsx +++ b/packages/manager/src/features/Volumes/VolumeDetails/VolumeEntityDetails/VolumeEntityDetailBody.tsx @@ -1,5 +1,5 @@ import { useProfile, useRegionsQuery } from '@linode/queries'; -import { Box, LinkButton, Typography } from '@linode/ui'; +import { Box, Typography } from '@linode/ui'; import { getFormattedStatus } from '@linode/utilities'; import Grid from '@mui/material/Grid'; import { useTheme } from '@mui/material/styles'; @@ -7,10 +7,10 @@ import React from 'react'; import Lock from 'src/assets/icons/lock.svg'; import Unlock from 'src/assets/icons/unlock.svg'; -import { Link } from 'src/components/Link'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { formatDate } from 'src/utilities/formatDate'; +import { AttachedToValue } from '../../Partials/AttachedToValue'; import { volumeStatusIconMap } from '../../utils'; import type { Volume } from '@linode/api-v4'; @@ -103,19 +103,7 @@ export const VolumeEntityDetailBody = ({ volume, detachHandler }: Props) => { Attached To ({ font: theme.font.bold })}> - {volume.linode_id !== null ? ( - - - {volume.linode_label} - - | Detach - - ) : ( - 'Unattached' - )} +