Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions app/pages/project/instances/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -122,15 +122,20 @@ export const useMakeInstanceActions = (
{
label: 'Reboot',
onActivate() {
rebootInstance(instanceParams, {
onSuccess: () =>
addToast(<>Rebooting instance <HL>{instance.name}</HL></>), // 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 <HL>{instance.name}</HL></>), // prettier-ignore
}),
modalTitle: 'Confirm reboot instance',
modalContent: (
<p>
Are you sure you want to reboot <HL>{instance.name}</HL>?
</p>
),
errorTitle: `Error rebooting ${instance.name}`,
})
},
disabled: !instanceCan.reboot(instance) && (
Expand Down Expand Up @@ -162,7 +167,7 @@ export const useMakeInstanceActions = (
},
]
},
[project, deleteInstanceAsync, navigate, rebootInstance]
[project, deleteInstanceAsync, navigate, rebootInstanceAsync]
)

return { makeButtonActions, makeMenuActions }
Expand Down
50 changes: 45 additions & 5 deletions test/e2e/instance.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'), {
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')

Expand Down
10 changes: 7 additions & 3 deletions test/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
Loading