Skip to content

Commit ee42197

Browse files
new: [STORIF-83] Action links added to the volume page. (linode#12822)
* new: [STORIF-83] Action links added to the volume page. * Added changeset: Action links to the Volume Details page
1 parent 7a1c537 commit ee42197

35 files changed

+345
-167
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Added
3+
---
4+
5+
Action links to the Volume Details page ([#12822](https://github.com/linode/manager/pull/12822))

packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeVolumes.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ import { TableSortCell } from 'src/components/TableSortCell';
2222
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
2323
import { DeleteVolumeDialog } from 'src/features/Volumes/Dialogs/DeleteVolumeDialog';
2424
import { DetachVolumeDialog } from 'src/features/Volumes/Dialogs/DetachVolumeDialog';
25-
import { CloneVolumeDrawer } from 'src/features/Volumes/Drawers/CloneVolumeDrawer';
26-
import { EditVolumeDrawer } from 'src/features/Volumes/Drawers/EditVolumeDrawer';
27-
import { ManageTagsDrawer } from 'src/features/Volumes/Drawers/ManageTagsDrawer';
28-
import { ResizeVolumeDrawer } from 'src/features/Volumes/Drawers/ResizeVolumeDrawer';
29-
import { VolumeDetailsDrawer } from 'src/features/Volumes/Drawers/VolumeDetailsDrawer';
30-
import { LinodeVolumeAddDrawer } from 'src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer';
31-
import { VolumeTableRow } from 'src/features/Volumes/VolumeTableRow';
25+
import { VolumeTableRow } from 'src/features/Volumes/Partials/VolumeTableRow';
26+
import { CloneVolumeDrawer } from 'src/features/Volumes/VolumeDrawers/CloneVolumeDrawer/CloneVolumeDrawer';
27+
import { EditVolumeDrawer } from 'src/features/Volumes/VolumeDrawers/EditVolumeDrawer/EditVolumeDrawer';
28+
import { ManageTagsDrawer } from 'src/features/Volumes/VolumeDrawers/ManageTagsDrawer/ManageTagsDrawer';
29+
import { ResizeVolumeDrawer } from 'src/features/Volumes/VolumeDrawers/ResizeVolumeDrawer';
30+
import { VolumeDetailsDrawer } from 'src/features/Volumes/VolumeDrawers/VolumeDetailsDrawer';
31+
import { LinodeVolumeAddDrawer } from 'src/features/Volumes/VolumeDrawers/VolumeDrawer/LinodeVolumeAddDrawer';
3232
import { useOrderV2 } from 'src/hooks/useOrderV2';
3333
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
3434

packages/manager/src/features/Volumes/VolumeTableRow.test.tsx renamed to packages/manager/src/features/Volumes/Partials/VolumeTableRow.test.tsx

File renamed without changes.

packages/manager/src/features/Volumes/VolumeTableRow.tsx renamed to packages/manager/src/features/Volumes/Partials/VolumeTableRow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import { TableRow } from 'src/components/TableRow';
1313
import { useFlags } from 'src/hooks/useFlags';
1414
import { useInProgressEvents } from 'src/queries/events/events';
1515

16-
import { HighPerformanceVolumeIcon } from '../Linodes/HighPerformanceVolumeIcon';
16+
import { HighPerformanceVolumeIcon } from '../../Linodes/HighPerformanceVolumeIcon';
1717
import {
1818
getDerivedVolumeStatusFromStatusAndEvent,
1919
getEventProgress,
2020
volumeStatusIconMap,
21-
} from './utils';
21+
} from '../utils';
2222
import { VolumesActionMenu } from './VolumesActionMenu';
2323

2424
import type { ActionHandlers } from './VolumesActionMenu';

packages/manager/src/features/Volumes/VolumesActionMenu.test.tsx renamed to packages/manager/src/features/Volumes/Partials/VolumesActionMenu.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { within } from '@testing-library/react';
12
import { userEvent } from '@testing-library/user-event';
23
import * as React from 'react';
34

@@ -114,4 +115,46 @@ describe('Volume action menu', () => {
114115

115116
expect(getByText('Delete')).toBeVisible();
116117
});
118+
119+
describe('Volume details version', () => {
120+
it('should display basic actions outside the elipsis', () => {
121+
const { getByText } = renderWithTheme(
122+
<VolumesActionMenu {...props} isVolumeDetails={true} />
123+
);
124+
125+
for (const action of ['Show Config', 'Resize']) {
126+
expect(getByText(action)).toBeVisible();
127+
}
128+
});
129+
130+
it('should not display basic actions inside the elipsis', () => {
131+
const { getByLabelText } = renderWithTheme(
132+
<VolumesActionMenu {...props} isVolumeDetails={true} />
133+
);
134+
135+
const actionMenuButton = getByLabelText(
136+
`Action menu for Volume ${volume.label}`
137+
);
138+
139+
const { queryByText } = within(actionMenuButton);
140+
141+
for (const action of ['Show Config', 'Resize']) {
142+
expect(queryByText(action)).not.toBeInTheDocument();
143+
}
144+
});
145+
146+
it('should not display Manage Tags action', async () => {
147+
const { getByLabelText, queryByText } = renderWithTheme(
148+
<VolumesActionMenu {...props} isVolumeDetails={true} />
149+
);
150+
151+
const actionMenuButton = getByLabelText(
152+
`Action menu for Volume ${volume.label}`
153+
);
154+
155+
await userEvent.click(actionMenuButton);
156+
157+
expect(queryByText('Manage Tags')).not.toBeInTheDocument();
158+
});
159+
});
117160
});

packages/manager/src/features/Volumes/VolumesActionMenu.tsx renamed to packages/manager/src/features/Volumes/Partials/VolumesActionMenu.tsx

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22

33
import { ActionMenu } from 'src/components/ActionMenu/ActionMenu';
4+
import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction';
45
import { getRestrictedResourceText } from 'src/features/Account/utils';
56
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
67

@@ -21,14 +22,14 @@ export interface ActionHandlers {
2122

2223
export interface Props {
2324
handlers: ActionHandlers;
25+
isVolumeDetails?: boolean;
2426
isVolumesLanding: boolean;
2527
volume: Volume;
2628
}
2729

2830
export const VolumesActionMenu = (props: Props) => {
29-
const { handlers, isVolumesLanding, volume } = props;
30-
31-
const attached = volume.linode_id !== null;
31+
const { handlers, isVolumesLanding, isVolumeDetails, volume } = props;
32+
const isAttached = volume.linode_id !== null;
3233

3334
const { data: accountPermissions } = usePermissions('account', [
3435
'create_volume',
@@ -47,12 +48,12 @@ export const VolumesActionMenu = (props: Props) => {
4748
volume.id
4849
);
4950

50-
const actions: Action[] = [
51-
{
51+
const ACTIONS = {
52+
SHOW_CONFIG: {
5253
onClick: handlers.handleDetails,
5354
title: 'Show Config',
5455
},
55-
{
56+
EDIT: {
5657
disabled: !volumePermissions?.update_volume,
5758
onClick: handlers.handleEdit,
5859
title: 'Edit',
@@ -64,12 +65,12 @@ export const VolumesActionMenu = (props: Props) => {
6465
})
6566
: undefined,
6667
},
67-
{
68+
MANAGE_TAGS: {
6869
disabled: !volumePermissions?.update_volume,
6970
onClick: handlers.handleManageTags,
7071
title: 'Manage Tags',
7172
},
72-
{
73+
RESIZE: {
7374
disabled: !volumePermissions?.resize_volume,
7475
onClick: handlers.handleResize,
7576
title: 'Resize',
@@ -81,7 +82,7 @@ export const VolumesActionMenu = (props: Props) => {
8182
})
8283
: undefined,
8384
},
84-
{
85+
CLONE: {
8586
disabled:
8687
!volumePermissions?.clone_volume || !accountPermissions?.create_volume,
8788
onClick: handlers.handleClone,
@@ -94,10 +95,7 @@ export const VolumesActionMenu = (props: Props) => {
9495
})
9596
: undefined,
9697
},
97-
];
98-
99-
if (!attached && isVolumesLanding) {
100-
actions.push({
98+
ATTACH: {
10199
disabled: !volumePermissions?.attach_volume,
102100
onClick: handlers.handleAttach,
103101
title: 'Attach',
@@ -108,9 +106,8 @@ export const VolumesActionMenu = (props: Props) => {
108106
resourceType: 'Volumes',
109107
})
110108
: undefined,
111-
});
112-
} else {
113-
actions.push({
109+
},
110+
DETACH: {
114111
disabled: !volumePermissions?.detach_volume,
115112
onClick: handlers.handleDetach,
116113
title: 'Detach',
@@ -121,28 +118,64 @@ export const VolumesActionMenu = (props: Props) => {
121118
resourceType: 'Volumes',
122119
})
123120
: undefined,
124-
});
121+
},
122+
DELETE: {
123+
disabled: !volumePermissions?.delete_volume || isAttached,
124+
onClick: handlers.handleDelete,
125+
title: 'Delete',
126+
tooltip: !volumePermissions?.delete_volume
127+
? getRestrictedResourceText({
128+
action: 'delete',
129+
isSingular: true,
130+
resourceType: 'Volumes',
131+
})
132+
: isAttached
133+
? 'Your volume must be detached before it can be deleted.'
134+
: undefined,
135+
},
136+
};
137+
138+
const actions: Action[] = [];
139+
140+
if (!isVolumeDetails) {
141+
actions.push(
142+
ACTIONS.SHOW_CONFIG,
143+
ACTIONS.EDIT,
144+
ACTIONS.MANAGE_TAGS,
145+
ACTIONS.RESIZE
146+
);
125147
}
126148

127-
actions.push({
128-
disabled: !volumePermissions?.delete_volume || attached,
129-
onClick: handlers.handleDelete,
130-
title: 'Delete',
131-
tooltip: !volumePermissions?.delete_volume
132-
? getRestrictedResourceText({
133-
action: 'delete',
134-
isSingular: true,
135-
resourceType: 'Volumes',
136-
})
137-
: attached
138-
? 'Your volume must be detached before it can be deleted.'
139-
: undefined,
140-
});
149+
actions.push(ACTIONS.CLONE);
150+
151+
const inlineActions: Action[] = [ACTIONS.SHOW_CONFIG, ACTIONS.RESIZE];
152+
153+
if (!isAttached && isVolumesLanding) {
154+
actions.push(ACTIONS.ATTACH);
155+
} else {
156+
actions.push(ACTIONS.DETACH);
157+
}
158+
159+
actions.push(ACTIONS.DELETE);
141160

142161
return (
143-
<ActionMenu
144-
actionsList={actions}
145-
ariaLabel={`Action menu for Volume ${volume.label}`}
146-
/>
162+
<div>
163+
{isVolumeDetails &&
164+
inlineActions.map((action) => {
165+
return (
166+
<InlineMenuAction
167+
actionText={action.title}
168+
disabled={action.disabled}
169+
key={action.title}
170+
onClick={action.onClick}
171+
tooltip={action.tooltip}
172+
/>
173+
);
174+
})}
175+
<ActionMenu
176+
actionsList={actions}
177+
ariaLabel={`Action menu for Volume ${volume.label}`}
178+
/>
179+
</div>
147180
);
148181
};

packages/manager/src/features/Volumes/VolumeCreate.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ import { reportAgreementSigningError } from 'src/utilities/reportAgreementSignin
5959

6060
import { usePermissions } from '../IAM/hooks/usePermissions';
6161
import { SIZE_FIELD_WIDTH } from './constants';
62-
import { ConfigSelect } from './Drawers/VolumeDrawer/ConfigSelect';
63-
import { SizeField } from './Drawers/VolumeDrawer/SizeField';
62+
import { ConfigSelect } from './VolumeDrawers/VolumeDrawer/ConfigSelect';
63+
import { SizeField } from './VolumeDrawers/VolumeDrawer/SizeField';
6464

6565
import type { APIError, Region, VolumeEncryption } from '@linode/api-v4';
6666
import type { Linode } from '@linode/api-v4/lib/linodes/types';

packages/manager/src/features/Volumes/VolumeDetails/VolumeDetails.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList';
1111
import { useFlags } from 'src/hooks/useFlags';
1212
import { useTabs } from 'src/hooks/useTabs';
1313

14+
import { VolumeDrawers } from '../VolumeDrawers/VolumeDrawers';
1415
import { VolumeDetailsHeader } from './VolumeDetailsHeader';
1516
import { VolumeEntityDetail } from './VolumeEntityDetails/VolumeEntityDetail';
1617

1718
export const VolumeDetails = () => {
1819
const navigate = useNavigate();
20+
1921
const { volumeSummaryPage } = useFlags();
2022
const { volumeId } = useParams({ from: '/volumes/$volumeId' });
2123
const { data: volume, isLoading, error } = useVolumeQuery(volumeId);
@@ -34,8 +36,15 @@ export const VolumeDetails = () => {
3436
return <CircleProgress />;
3537
}
3638

39+
const navigateToVolumeSummary = () => {
40+
navigate({
41+
search: (prev) => prev,
42+
to: `/volumes/${volume.id}/summary`,
43+
});
44+
};
45+
3746
if (location.pathname === `/volumes/${volumeId}`) {
38-
navigate({ to: `/volumes/${volumeId}/summary` });
47+
navigateToVolumeSummary();
3948
}
4049

4150
return (
@@ -52,6 +61,8 @@ export const VolumeDetails = () => {
5261
</TabPanels>
5362
</React.Suspense>
5463
</Tabs>
64+
65+
<VolumeDrawers onCloseHandler={navigateToVolumeSummary} />
5566
</>
5667
);
5768
};

packages/manager/src/features/Volumes/VolumeDetails/VolumeEntityDetails/VolumeEntityDetail.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22

33
import { EntityDetail } from 'src/components/EntityDetail/EntityDetail';
44

5+
import { useVolumeActionHandlers } from '../../hooks/useVolumeActionHandlers';
56
import { VolumeEntityDetailBody } from './VolumeEntityDetailBody';
67
import { VolumeEntityDetailFooter } from './VolumeEntityDetailFooter';
78
import { VolumeEntityDetailHeader } from './VolumeEntityDetailHeader';
@@ -11,13 +12,23 @@ import type { Volume } from '@linode/api-v4';
1112
interface Props {
1213
volume: Volume;
1314
}
14-
1515
export const VolumeEntityDetail = ({ volume }: Props) => {
16+
const { getActionHandlers } = useVolumeActionHandlers(
17+
'/volumes/$volumeId/summary'
18+
);
19+
20+
const handlers = getActionHandlers(volume.id);
21+
1622
return (
1723
<EntityDetail
18-
body={<VolumeEntityDetailBody volume={volume} />}
24+
body={
25+
<VolumeEntityDetailBody
26+
detachHandler={handlers.handleDetach}
27+
volume={volume}
28+
/>
29+
}
1930
footer={<VolumeEntityDetailFooter volume={volume} />}
20-
header={<VolumeEntityDetailHeader volume={volume} />}
31+
header={<VolumeEntityDetailHeader handlers={handlers} volume={volume} />}
2132
/>
2233
);
2334
};

packages/manager/src/features/Volumes/VolumeDetails/VolumeEntityDetails/VolumeEntityDetailBody.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useProfile, useRegionsQuery } from '@linode/queries';
2-
import { Box, Typography } from '@linode/ui';
2+
import { Box, StyledLinkButton, Typography } from '@linode/ui';
33
import { getFormattedStatus } from '@linode/utilities';
44
import Grid from '@mui/material/Grid';
55
import { useTheme } from '@mui/material/styles';
@@ -16,10 +16,11 @@ import { volumeStatusIconMap } from '../../utils';
1616
import type { Volume } from '@linode/api-v4';
1717

1818
interface Props {
19+
detachHandler: () => void;
1920
volume: Volume;
2021
}
2122

22-
export const VolumeEntityDetailBody = ({ volume }: Props) => {
23+
export const VolumeEntityDetailBody = ({ volume, detachHandler }: Props) => {
2324
const theme = useTheme();
2425
const { data: profile } = useProfile();
2526
const { data: regions } = useRegionsQuery();
@@ -103,12 +104,18 @@ export const VolumeEntityDetailBody = ({ volume }: Props) => {
103104
<Typography>Attached To</Typography>
104105
<Typography sx={(theme) => ({ font: theme.font.bold })}>
105106
{volume.linode_id !== null ? (
106-
<Link
107-
className="link secondaryLink"
108-
to={`/linodes/${volume.linode_id}/storage`}
109-
>
110-
{volume.linode_label}
111-
</Link>
107+
<Box sx={{ display: 'flex', gap: theme.spacingFunction(8) }}>
108+
<Link
109+
className="link secondaryLink"
110+
to={`/linodes/${volume.linode_id}/storage`}
111+
>
112+
{volume.linode_label}
113+
</Link>
114+
|
115+
<StyledLinkButton onClick={detachHandler}>
116+
Detach
117+
</StyledLinkButton>
118+
</Box>
112119
) : (
113120
'Unattached'
114121
)}

0 commit comments

Comments
 (0)