Skip to content

Commit 9b60d5c

Browse files
authored
Add confirmation step to instance reboot process (#2559)
* Confirm instance reboot * Add test re: rebooting instances * Add helper function to help with testing menu actions * Remove duplicate expectations * Updated copy in modal
1 parent 1e0486c commit 9b60d5c

File tree

3 files changed

+67
-18
lines changed

3 files changed

+67
-18
lines changed

app/pages/project/instances/actions.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const useMakeInstanceActions = (
4141
const opts = { onSuccess: options.onSuccess }
4242
const { mutateAsync: startInstanceAsync } = useApiMutation('instanceStart', opts)
4343
const { mutateAsync: stopInstanceAsync } = useApiMutation('instanceStop', opts)
44-
const { mutate: rebootInstance } = useApiMutation('instanceReboot', opts)
44+
const { mutateAsync: rebootInstanceAsync } = useApiMutation('instanceReboot', opts)
4545
// delete has its own
4646
const { mutateAsync: deleteInstanceAsync } = useApiMutation('instanceDelete', {
4747
onSuccess: options.onDelete,
@@ -122,15 +122,20 @@ export const useMakeInstanceActions = (
122122
{
123123
label: 'Reboot',
124124
onActivate() {
125-
rebootInstance(instanceParams, {
126-
onSuccess: () =>
127-
addToast(<>Rebooting instance <HL>{instance.name}</HL></>), // prettier-ignore
128-
onError: (error) =>
129-
addToast({
130-
variant: 'error',
131-
title: `Error rebooting instance '${instance.name}'`,
132-
content: error.message,
125+
confirmAction({
126+
actionType: 'danger',
127+
doAction: () =>
128+
rebootInstanceAsync(instanceParams, {
129+
onSuccess: () =>
130+
addToast(<>Rebooting instance <HL>{instance.name}</HL></>), // prettier-ignore
133131
}),
132+
modalTitle: 'Confirm reboot instance',
133+
modalContent: (
134+
<p>
135+
Are you sure you want to reboot <HL>{instance.name}</HL>?
136+
</p>
137+
),
138+
errorTitle: `Error rebooting ${instance.name}`,
134139
})
135140
},
136141
disabled: !instanceCan.reboot(instance) && (
@@ -162,7 +167,7 @@ export const useMakeInstanceActions = (
162167
},
163168
]
164169
},
165-
[project, deleteInstanceAsync, navigate, rebootInstance]
170+
[project, deleteInstanceAsync, navigate, rebootInstanceAsync]
166171
)
167172

168173
return { makeButtonActions, makeMenuActions }

test/e2e/instance.e2e.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { clickRowAction, expect, expectRowVisible, test, type Page } from './utils'
8+
import {
9+
clickRowAction,
10+
expect,
11+
expectRowVisible,
12+
openRowActions,
13+
test,
14+
type Page,
15+
} from './utils'
916

1017
const expectInstanceState = async (page: Page, instance: string, state: string) => {
1118
await expectRowVisible(page.getByRole('table'), {
@@ -33,10 +40,7 @@ test('can start a failed instance', async ({ page }) => {
3340
await page.goto('/projects/mock-project/instances')
3441

3542
// check the start button disabled message on a running instance
36-
await page
37-
.getByRole('row', { name: 'db1', exact: false })
38-
.getByRole('button', { name: 'Row actions' })
39-
.click()
43+
await openRowActions(page, 'db1')
4044
await page.getByRole('menuitem', { name: 'Start' }).hover()
4145
await expect(
4246
page.getByText('Only stopped or failed instances can be started')
@@ -99,6 +103,42 @@ test('can stop a starting instance, then start it again', async ({ page }) => {
99103
await expectInstanceState(page, 'not-there-yet', 'running')
100104
})
101105

106+
test('can reboot a running instance', async ({ page }) => {
107+
await page.goto('/projects/mock-project/instances')
108+
await expect(page).toHaveTitle('Instances / mock-project / Projects / Oxide Console')
109+
110+
await expectInstanceState(page, 'db1', 'running')
111+
await clickRowAction(page, 'db1', 'Reboot')
112+
await page.getByRole('button', { name: 'Confirm' }).click()
113+
await expectInstanceState(page, 'db1', 'rebooting')
114+
await expectInstanceState(page, 'db1', 'running')
115+
})
116+
117+
test('cannot reboot a failed instance', async ({ page }) => {
118+
await page.goto('/projects/mock-project/instances')
119+
await expectInstanceState(page, 'you-fail', 'failed')
120+
await openRowActions(page, 'you-fail')
121+
await expect(page.getByRole('menuitem', { name: 'Reboot' })).toBeDisabled()
122+
})
123+
124+
test('cannot reboot a starting instance, or a stopped instance', async ({ page }) => {
125+
await page.goto('/projects/mock-project/instances')
126+
await expectInstanceState(page, 'not-there-yet', 'starting')
127+
await openRowActions(page, 'not-there-yet')
128+
await expect(page.getByRole('menuitem', { name: 'Reboot' })).toBeDisabled()
129+
// hit escape to close the menu so clickRowAction succeeds
130+
await page.keyboard.press('Escape')
131+
132+
// stop it so we can try rebooting a stopped instance
133+
await clickRowAction(page, 'not-there-yet', 'Stop')
134+
await page.getByRole('button', { name: 'Confirm' }).click()
135+
await expectInstanceState(page, 'not-there-yet', 'stopping')
136+
await expectInstanceState(page, 'not-there-yet', 'stopped')
137+
// reboot is still disabled for a stopped instance
138+
await openRowActions(page, 'not-there-yet')
139+
await expect(page.getByRole('menuitem', { name: 'Reboot' })).toBeDisabled()
140+
})
141+
102142
test('delete from instance detail', async ({ page }) => {
103143
await page.goto('/projects/mock-project/instances/you-fail')
104144

test/e2e/utils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,16 @@ export async function closeToast(page: Page) {
145145
export const clipboardText = async (page: Page) =>
146146
page.evaluate(() => navigator.clipboard.readText())
147147

148-
/** Select row by `rowText`, click the row actions button, and click `actionName` */
149-
export async function clickRowAction(page: Page, rowText: string, actionName: string) {
148+
export const openRowActions = async (page: Page, name: string) => {
150149
await page
151-
.getByRole('row', { name: rowText, exact: false })
150+
.getByRole('row', { name, exact: false })
152151
.getByRole('button', { name: 'Row actions' })
153152
.click()
153+
}
154+
155+
/** Select row by `rowName`, click the row actions button, and click `actionName` */
156+
export async function clickRowAction(page: Page, rowName: string, actionName: string) {
157+
await openRowActions(page, rowName)
154158
await page.getByRole('menuitem', { name: actionName }).click()
155159
}
156160

0 commit comments

Comments
 (0)