Skip to content

Commit b2b3a74

Browse files
authored
Add snapshot action to instance disks list (#1774)
add snapshot action to instance disks
1 parent 0f01550 commit b2b3a74

File tree

3 files changed

+63
-3
lines changed

3 files changed

+63
-3
lines changed

app/pages/project/instances/instance/tabs/StorageTab.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { LoaderFunctionArgs } from 'react-router-dom'
1111
import {
1212
type Disk,
1313
apiQueryClient,
14+
diskCan,
15+
genName,
1416
instanceCan,
1517
useApiMutation,
1618
useApiQueryClient,
@@ -60,11 +62,35 @@ export function StorageTab() {
6062
)
6163

6264
const detachDisk = useApiMutation('instanceDiskDetach', {})
65+
const createSnapshot = useApiMutation('snapshotCreate', {
66+
onSuccess() {
67+
queryClient.invalidateQueries('snapshotList')
68+
addToast({ content: 'Snapshot successfully created' })
69+
},
70+
})
6371

6472
const { data: instance } = usePrefetchedApiQuery('instanceView', instancePathQuery)
6573

6674
const makeActions = useCallback(
6775
(disk: Disk): MenuAction[] => [
76+
{
77+
label: 'Snapshot',
78+
disabled: !diskCan.snapshot(disk) && (
79+
<>
80+
Only disks in state {fancifyStates(diskCan.snapshot.states)} can be snapshotted
81+
</>
82+
),
83+
onActivate() {
84+
createSnapshot.mutate({
85+
query: { project },
86+
body: {
87+
name: genName(disk.name),
88+
disk: disk.name,
89+
description: '',
90+
},
91+
})
92+
},
93+
},
6894
{
6995
label: 'Detach',
7096
disabled: !instanceCan.detachDisk(instance) && (
@@ -82,7 +108,7 @@ export function StorageTab() {
82108
},
83109
},
84110
],
85-
[detachDisk, instance, queryClient, instancePathQuery]
111+
[detachDisk, instance, queryClient, instancePathQuery, createSnapshot, project]
86112
)
87113

88114
const attachDisk = useApiMutation('instanceDiskAttach', {

app/test/e2e/instance/attach-disk.e2e.ts renamed to app/test/e2e/instance/disks.e2e.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { expect, expectNotVisible, expectVisible, stopInstance, test } from '../utils'
8+
import {
9+
clickRowAction,
10+
expect,
11+
expectNotVisible,
12+
expectRowVisible,
13+
expectVisible,
14+
stopInstance,
15+
test,
16+
} from '../utils'
917

1018
test('Attach disk', async ({ page }) => {
1119
await page.goto('/projects/mock-project/instances/db1')
@@ -57,3 +65,23 @@ test('Attach disk', async ({ page }) => {
5765
await page.click('role=button[name="Attach Disk"]')
5866
await expectVisible(page, ['role=cell[name="disk-3"]'])
5967
})
68+
69+
test('Snapshot disk', async ({ page }) => {
70+
await page.goto('/projects/mock-project/instances/db1')
71+
72+
// have to use nth with toasts because the text shows up in multiple spots
73+
const successMsg = page.getByText('Snapshot successfully created').nth(0)
74+
await expect(successMsg).toBeHidden()
75+
76+
await clickRowAction(page, 'disk-2', 'Snapshot')
77+
78+
await expect(successMsg).toBeVisible() // we see the toast!
79+
80+
// now go see the snapshot on the snapshots page
81+
await page.getByRole('link', { name: 'Snapshots' }).click()
82+
const table = page.getByRole('table')
83+
await expectRowVisible(table, {
84+
name: expect.stringMatching(/^disk-2-/),
85+
disk: 'disk-2',
86+
})
87+
})

app/test/e2e/utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ export async function expectNotVisible(page: Page, selectors: Selector[]) {
4747
}
4848
}
4949

50+
// Technically this has type AsymmetricMatcher, which is not exported by
51+
// Playwright and is (surprisingly) just Record<string, any>. Rather than use
52+
// that, I think it's smarter to do the following in case they ever make the
53+
// type more interesting; this will still do what it's supposed to.
54+
type StringMatcher = ReturnType<typeof expect.stringMatching>
55+
5056
/**
5157
* Assert that a row matching `expectedRow` is present in `table`. The match
5258
* uses `objectContaining`, so `expectedRow` does not need to contain every
@@ -55,7 +61,7 @@ export async function expectNotVisible(page: Page, selectors: Selector[]) {
5561
*/
5662
export async function expectRowVisible(
5763
table: Locator,
58-
expectedRow: Record<string, string>
64+
expectedRow: Record<string, string | StringMatcher>
5965
) {
6066
// wait for header and rows to avoid flake town
6167
const headerLoc = table.locator('thead >> role=cell')

0 commit comments

Comments
 (0)