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'
- )}
+