diff --git a/app/pages/project/instances/actions.tsx b/app/pages/project/instances/actions.tsx
index 6b50afd038..b59e09d45a 100644
--- a/app/pages/project/instances/actions.tsx
+++ b/app/pages/project/instances/actions.tsx
@@ -41,7 +41,7 @@ export const useMakeInstanceActions = (
const opts = { onSuccess: options.onSuccess }
const { mutateAsync: startInstanceAsync } = useApiMutation('instanceStart', opts)
const { mutateAsync: stopInstanceAsync } = useApiMutation('instanceStop', opts)
- const { mutate: rebootInstance } = useApiMutation('instanceReboot', opts)
+ const { mutateAsync: rebootInstanceAsync } = useApiMutation('instanceReboot', opts)
// delete has its own
const { mutateAsync: deleteInstanceAsync } = useApiMutation('instanceDelete', {
onSuccess: options.onDelete,
@@ -122,15 +122,20 @@ export const useMakeInstanceActions = (
{
label: 'Reboot',
onActivate() {
- rebootInstance(instanceParams, {
- onSuccess: () =>
- addToast(<>Rebooting instance {instance.name}>), // prettier-ignore
- onError: (error) =>
- addToast({
- variant: 'error',
- title: `Error rebooting instance '${instance.name}'`,
- content: error.message,
+ confirmAction({
+ actionType: 'danger',
+ doAction: () =>
+ rebootInstanceAsync(instanceParams, {
+ onSuccess: () =>
+ addToast(<>Rebooting instance {instance.name}>), // prettier-ignore
}),
+ modalTitle: 'Confirm reboot instance',
+ modalContent: (
+
+ Are you sure you want to reboot {instance.name}?
+
+ ),
+ errorTitle: `Error rebooting ${instance.name}`,
})
},
disabled: !instanceCan.reboot(instance) && (
@@ -162,7 +167,7 @@ export const useMakeInstanceActions = (
},
]
},
- [project, deleteInstanceAsync, navigate, rebootInstance]
+ [project, deleteInstanceAsync, navigate, rebootInstanceAsync]
)
return { makeButtonActions, makeMenuActions }
diff --git a/test/e2e/instance.e2e.ts b/test/e2e/instance.e2e.ts
index 8da3ba836f..e011157bbf 100644
--- a/test/e2e/instance.e2e.ts
+++ b/test/e2e/instance.e2e.ts
@@ -5,7 +5,14 @@
*
* Copyright Oxide Computer Company
*/
-import { clickRowAction, expect, expectRowVisible, test, type Page } from './utils'
+import {
+ clickRowAction,
+ expect,
+ expectRowVisible,
+ openRowActions,
+ test,
+ type Page,
+} from './utils'
const expectInstanceState = async (page: Page, instance: string, state: string) => {
await expectRowVisible(page.getByRole('table'), {
@@ -33,10 +40,7 @@ test('can start a failed instance', async ({ page }) => {
await page.goto('/projects/mock-project/instances')
// check the start button disabled message on a running instance
- await page
- .getByRole('row', { name: 'db1', exact: false })
- .getByRole('button', { name: 'Row actions' })
- .click()
+ await openRowActions(page, 'db1')
await page.getByRole('menuitem', { name: 'Start' }).hover()
await expect(
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 }) => {
await expectInstanceState(page, 'not-there-yet', 'running')
})
+test('can reboot a running instance', async ({ page }) => {
+ await page.goto('/projects/mock-project/instances')
+ await expect(page).toHaveTitle('Instances / mock-project / Projects / Oxide Console')
+
+ await expectInstanceState(page, 'db1', 'running')
+ await clickRowAction(page, 'db1', 'Reboot')
+ await page.getByRole('button', { name: 'Confirm' }).click()
+ await expectInstanceState(page, 'db1', 'rebooting')
+ await expectInstanceState(page, 'db1', 'running')
+})
+
+test('cannot reboot a failed instance', async ({ page }) => {
+ await page.goto('/projects/mock-project/instances')
+ await expectInstanceState(page, 'you-fail', 'failed')
+ await openRowActions(page, 'you-fail')
+ await expect(page.getByRole('menuitem', { name: 'Reboot' })).toBeDisabled()
+})
+
+test('cannot reboot a starting instance, or a stopped instance', async ({ page }) => {
+ await page.goto('/projects/mock-project/instances')
+ await expectInstanceState(page, 'not-there-yet', 'starting')
+ await openRowActions(page, 'not-there-yet')
+ await expect(page.getByRole('menuitem', { name: 'Reboot' })).toBeDisabled()
+ // hit escape to close the menu so clickRowAction succeeds
+ await page.keyboard.press('Escape')
+
+ // stop it so we can try rebooting a stopped instance
+ await clickRowAction(page, 'not-there-yet', 'Stop')
+ await page.getByRole('button', { name: 'Confirm' }).click()
+ await expectInstanceState(page, 'not-there-yet', 'stopping')
+ await expectInstanceState(page, 'not-there-yet', 'stopped')
+ // reboot is still disabled for a stopped instance
+ await openRowActions(page, 'not-there-yet')
+ await expect(page.getByRole('menuitem', { name: 'Reboot' })).toBeDisabled()
+})
+
test('delete from instance detail', async ({ page }) => {
await page.goto('/projects/mock-project/instances/you-fail')
diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts
index 9cb7864751..2f8437d0cd 100644
--- a/test/e2e/utils.ts
+++ b/test/e2e/utils.ts
@@ -145,12 +145,16 @@ export async function closeToast(page: Page) {
export const clipboardText = async (page: Page) =>
page.evaluate(() => navigator.clipboard.readText())
-/** Select row by `rowText`, click the row actions button, and click `actionName` */
-export async function clickRowAction(page: Page, rowText: string, actionName: string) {
+export const openRowActions = async (page: Page, name: string) => {
await page
- .getByRole('row', { name: rowText, exact: false })
+ .getByRole('row', { name, exact: false })
.getByRole('button', { name: 'Row actions' })
.click()
+}
+
+/** Select row by `rowName`, click the row actions button, and click `actionName` */
+export async function clickRowAction(page: Page, rowName: string, actionName: string) {
+ await openRowActions(page, rowName)
await page.getByRole('menuitem', { name: actionName }).click()
}