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
31 changes: 24 additions & 7 deletions app/pages/project/instances/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { useNavigate } from 'react-router-dom'

import { instanceCan, useApiMutation, type Instance } from '@oxide/api'

import { HL } from '~/components/HL'
import { confirmAction } from '~/stores/confirm-action'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import type { MakeActions } from '~/table/columns/action-col'
Expand Down Expand Up @@ -65,14 +67,28 @@ export const useMakeInstanceActions = (
{
label: 'Stop',
onActivate() {
stopInstance.mutate(instanceParams, {
onSuccess: () => addToast({ title: `Stopping instance '${instance.name}'` }),
onError: (error) =>
addToast({
variant: 'error',
title: `Error stopping instance '${instance.name}'`,
content: error.message,
confirmAction({
actionType: 'danger',
doAction: async () =>
stopInstance.mutate(instanceParams, {
onSuccess: () =>
addToast({ title: `Stopping instance '${instance.name}'` }),
onError: (error) =>
addToast({
variant: 'error',
title: `Error stopping instance '${instance.name}'`,
content: error.message,
}),
}),
modalTitle: 'Confirm stop instance',
modalContent: (
<p>
Are you sure you want to stop <HL>{instance.name}</HL>? Stopped instances
retain attached disks and IP addresses, but allocated CPU and memory are
freed.
</p>
),
errorTitle: `Could not stop ${instance.name}`,
})
},
disabled: !instanceCan.stop(instance) && (
Expand Down Expand Up @@ -113,6 +129,7 @@ export const useMakeInstanceActions = (
},
}),
label: instance.name,
resourceKind: 'instance',
}),
disabled: !instanceCan.delete(instance) && (
<>Only {fancifyStates(instanceCan.delete.states)} instances can be deleted</>
Expand Down
6 changes: 4 additions & 2 deletions app/stores/confirm-delete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,20 @@ type DeleteConfig = {
* directly.
*/
label: React.ReactNode
resourceKind?: string
}

export const confirmDelete =
({ doDelete, label }: DeleteConfig) =>
({ doDelete, label, resourceKind }: DeleteConfig) =>
() => {
const displayLabel = typeof label === 'string' ? <HL>{label}</HL> : label
const modalTitle = resourceKind ? `Confirm delete ${resourceKind}` : 'Confirm delete'
useConfirmAction.setState({
actionConfig: {
doAction: doDelete,
modalContent: <p>Are you sure you want to delete {displayLabel}?</p>,
errorTitle: 'Could not delete resource',
modalTitle: 'Confirm delete',
modalTitle,
actionType: 'danger',
},
})
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/instance/list.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ test('can stop and delete a running instance', async ({ page }) => {
await row.getByRole('button', { name: 'Row actions' }).click()
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeDisabled()
await page.getByRole('menuitem', { name: 'Stop' }).click()
await page.getByRole('button', { name: 'Confirm' }).click()

// now it's stopped
await expect(row.getByRole('cell', { name: /stopped/ })).toBeVisible()
Expand All @@ -55,6 +56,7 @@ test('can stop a starting instance', async ({ page }) => {

await row.getByRole('button', { name: 'Row actions' }).click()
await page.getByRole('menuitem', { name: 'Stop' }).click()
await page.getByRole('button', { name: 'Confirm' }).click()

await expect(row.getByRole('cell', { name: /stopped/ })).toBeVisible()
})
2 changes: 2 additions & 0 deletions test/e2e/network-interface-create.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test('can create a NIC with a specified IP address', async ({ page }) => {
// stop the instance
await page.getByRole('button', { name: 'Instance actions' }).click()
await page.getByRole('menuitem', { name: 'Stop' }).click()
await page.getByRole('button', { name: 'Confirm' }).click()

// open the add network interface side modal
await page.getByRole('button', { name: 'Add network interface' }).click()
Expand Down Expand Up @@ -45,6 +46,7 @@ test('can create a NIC with a blank IP address', async ({ page }) => {
// stop the instance
await page.getByRole('button', { name: 'Instance actions' }).click()
await page.getByRole('menuitem', { name: 'Stop' }).click()
await page.getByRole('button', { name: 'Confirm' }).click()

// open the add network interface side modal
await page.getByRole('button', { name: 'Add network interface' }).click()
Expand Down
1 change: 1 addition & 0 deletions test/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export async function expectRowVisible(
export async function stopInstance(page: Page) {
await page.click('role=button[name="Instance actions"]')
await page.click('role=menuitem[name="Stop"]')
await page.click('role=button[name="Confirm"]')
await closeToast(page)
}

Expand Down
1 change: 1 addition & 0 deletions test/e2e/z-index.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ test('Dropdown content in SidebarModal shows on screen', async ({ page }) => {
// stop the instance
await page.getByRole('button', { name: 'Instance actions' }).click()
await page.getByRole('menuitem', { name: 'Stop' }).click()
await page.getByRole('button', { name: 'Confirm' }).click()

// open the add network interface side modal
await page.getByRole('button', { name: 'Add network interface' }).click()
Expand Down