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
33 changes: 33 additions & 0 deletions app/pages/project/instances/NetworkingTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
useInstanceSelector,
useProjectSelector,
} from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { confirmAction } from '~/stores/confirm-action'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
Expand Down Expand Up @@ -641,6 +642,38 @@ export default function NetworkingTab() {
const subnetDisabledReason =
availableSubnets.length === 0 ? 'No available external subnets' : null

useQuickActions(
() =>
[
!ephemeralDisabledReason && {
value: 'Attach ephemeral IP',
navGroup: 'Actions',
action: () => setAttachEphemeralModalOpen(true),
},
!floatingDisabledReason && {
value: 'Attach floating IP',
navGroup: 'Actions',
action: () => setAttachFloatingModalOpen(true),
},
instanceCan.updateNic({ runState: instance.runState }) && {
value: 'Add network interface',
navGroup: 'Actions',
action: () => setCreateModalOpen(true),
},
!subnetDisabledReason && {
value: 'Attach external subnet',
navGroup: 'Actions',
action: () => setAttachSubnetModalOpen(true),
},
].filter((x) => !!x),
[
ephemeralDisabledReason,
floatingDisabledReason,
instance.runState,
subnetDisabledReason,
]
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d want to make sure these live conditional ones update the way you’d expect. I think they will, but it’s good to check.

return (
<div className="space-y-5">
<CardBlock>
Expand Down
21 changes: 21 additions & 0 deletions app/pages/project/instances/StorageTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { DiskStateBadge, DiskTypeBadge, ReadOnlyBadge } from '~/components/State
import { AttachDiskModalForm } from '~/forms/disk-attach'
import { CreateDiskSideModalForm } from '~/forms/disk-create'
import { getInstanceSelector, useInstanceSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { DiskDetailSideModal } from '~/pages/project/disks/DiskDetailSideModal'
import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
Expand Down Expand Up @@ -341,6 +342,26 @@ export default function StorageTab() {
getCoreRowModel: getCoreRowModel(),
})

const canAttachDisk = instanceCan.attachDisk(instance)
useQuickActions(
() =>
canAttachDisk
? [
{
value: 'Attach existing disk',
navGroup: 'Actions',
action: () => setShowDiskAttach(true),
},
{
value: 'Create disk',
navGroup: 'Actions',
action: () => setShowDiskCreate(true),
},
]
: [],
[canAttachDisk]
)

return (
<div className="space-y-5">
<CardBlock>
Expand Down
17 changes: 16 additions & 1 deletion app/pages/project/vpcs/RouterPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import { routeFormMessage } from '~/forms/vpc-router-route-common'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
import { TypeValueCell } from '~/table/cells/TypeValueCell'
Expand Down Expand Up @@ -104,7 +105,7 @@ export default function RouterPage() {
icon={<Networking24Icon />}
title="No routes"
body="Add a route to see it here"
buttonText="Add route"
buttonText="New route"
buttonTo={pb.vpcRouterRoutesNew({ project, vpc, router })}
/>
)
Expand Down Expand Up @@ -176,6 +177,20 @@ export default function RouterPage() {
// https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L201-L205
const canCreateNewRoute = routerData.kind === 'custom'

useQuickActions(
() =>
canCreateNewRoute
? [
{
value: 'New route',
navGroup: 'Actions',
action: pb.vpcRouterRoutesNew({ project, vpc, router }),
},
]
: [],
[canCreateNewRoute, project, vpc, router]
)

return (
<>
<PageHeader>
Expand Down
6 changes: 6 additions & 0 deletions app/pages/settings/SSHKeysPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Key16Icon, Key24Icon } from '@oxide/design-system/icons/react'
import { DocsPopover } from '~/components/DocsPopover'
import { HL } from '~/components/HL'
import { makeCrumb } from '~/hooks/use-crumbs'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import { makeLinkCell } from '~/table/cells/LinkCell'
Expand Down Expand Up @@ -89,6 +90,11 @@ export default function SSHKeysPage() {
onClick={() => navigate(pb.sshKeysNew())}
/>
)
useQuickActions(
() => [{ value: 'Add SSH key', navGroup: 'Actions', action: pb.sshKeysNew() }],
[]
)

const { table } = useQueryTable({ query: sshKeyList, columns, emptyState })

return (
Expand Down
12 changes: 12 additions & 0 deletions app/pages/system/FleetAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
FleetAccessEditUserSideModal,
} from '~/forms/fleet-access'
import { useCurrentUser } from '~/hooks/use-current-user'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import { getActionsCol } from '~/table/columns/action-col'
Expand Down Expand Up @@ -239,6 +240,17 @@ export default function FleetAccessPage() {
[fleetPolicy, updatePolicy, me, navigate]
)

useQuickActions(
() => [
{
value: 'Add user or group',
navGroup: 'Actions',
action: () => setAddModalOpen(true),
},
],
[]
)

const tableInstance = useReactTable({
columns,
data: rows,
Expand Down
15 changes: 15 additions & 0 deletions app/pages/system/networking/IpPoolPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import { QueryParamTabs } from '~/components/QueryParamTabs'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getIpPoolSelector, useIpPoolSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { confirmAction } from '~/stores/confirm-action'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
Expand Down Expand Up @@ -274,6 +275,13 @@ function IpRangesTable() {
],
[pool, removeRange]
)
useQuickActions(
() => [
{ value: 'Add range', navGroup: 'Actions', action: pb.ipPoolRangeAdd({ pool }) },
],
[pool]
)

const columns = useColsWithActions(ipRangesStaticCols, makeRangeActions)
const { table } = useQueryTable({ query: ipPoolRangeList({ pool }), columns, emptyState })

Expand Down Expand Up @@ -416,6 +424,13 @@ function LinkedSilosTable() {

const [showLinkModal, setShowLinkModal] = useState(false)

useQuickActions(
() => [
{ value: 'Link silo', navGroup: 'Actions', action: () => setShowLinkModal(true) },
],
[]
)

const emptyState = (
<EmptyMessage
icon={<IpGlobal24Icon />}
Expand Down
19 changes: 19 additions & 0 deletions app/pages/system/networking/SubnetPoolPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import { QueryParamTabs } from '~/components/QueryParamTabs'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getSubnetPoolSelector, useSubnetPoolSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { confirmAction } from '~/stores/confirm-action'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
Expand Down Expand Up @@ -233,6 +234,17 @@ function MembersTable() {
],
[subnetPool, removeMember]
)
useQuickActions(
() => [
{
value: 'Add member',
navGroup: 'Actions',
action: pb.subnetPoolMemberAdd({ subnetPool }),
},
],
[subnetPool]
)

const columns = useColsWithActions(membersStaticCols, makeMemberActions)
const { table } = useQueryTable({
query: subnetPoolMemberList({ subnetPool }),
Expand Down Expand Up @@ -398,6 +410,13 @@ function LinkedSilosTable() {

const [showLinkModal, setShowLinkModal] = useState(false)

useQuickActions(
() => [
{ value: 'Link silo', navGroup: 'Actions', action: () => setShowLinkModal(true) },
],
[]
)

const emptyState = (
<EmptyMessage
icon={<Subnet24Icon />}
Expand Down
8 changes: 8 additions & 0 deletions app/pages/system/silos/SiloIdpsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Badge } from '@oxide/design-system/ui'
import { api, getListQFn, queryClient, type IdentityProvider } from '~/api'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getSiloSelector, useSiloSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { LinkCell } from '~/table/cells/LinkCell'
import { Columns } from '~/table/columns/common'
import { useQueryTable } from '~/table/QueryTable'
Expand Down Expand Up @@ -59,6 +60,13 @@ export default function SiloIdpsTab() {
[silo]
)

useQuickActions(
() => [
{ value: 'New provider', navGroup: 'Actions', action: pb.siloIdpsNew({ silo }) },
],
[silo]
)

const { table } = useQueryTable({
query: siloIdpList(silo),
columns,
Expand Down
15 changes: 15 additions & 0 deletions app/pages/system/silos/SiloScimTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from '~/api'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getSiloSelector, useSiloSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
Expand Down Expand Up @@ -103,6 +104,20 @@ export default function SiloScimTab() {

const [modalState, setModalState] = useState<ModalState>(false)

useQuickActions(
() =>
tokensResult.type === 'success'
? [
{
value: 'Create token',
navGroup: 'Actions',
action: () => setModalState({ kind: 'create' }),
},
]
: [],
[tokensResult.type]
)

return (
<>
<CardBlock>
Expand Down
70 changes: 69 additions & 1 deletion test/e2e/action-menu.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { expect, test, type Page } from '@playwright/test'

import { expectNotVisible } from './utils'
import { expectNotVisible, getPageAsUser, stopInstance } from './utils'

const openActionMenu = async (page: Page) => {
// open the action menu (use the sidenav button, as keyboard events aren't reliable in Playwright)
Expand Down Expand Up @@ -107,3 +107,71 @@ test('dismiss with Escape', async ({ page }) => {
await page.keyboard.press('Escape')
await expect(page.getByText('Enterto submit')).toBeHidden()
})

test('router quick action "New route" hidden for system router, visible for custom', async ({
page,
}) => {
// system router: action should not appear
await page.goto('/projects/mock-project/vpcs/mock-vpc/routers/mock-system-router')
await openActionMenu(page)
await expect(page.getByRole('option', { name: 'New route' })).toBeHidden()
await page.keyboard.press('Escape')

// custom router: action should appear
await page.goto('/projects/mock-project/vpcs/mock-vpc/routers/mock-custom-router')
await openActionMenu(page)
await expect(page.getByRole('option', { name: 'New route' })).toBeVisible()
})

test('storage tab quick actions hidden when instance running, visible when stopped', async ({
page,
}) => {
await page.goto('/projects/mock-project/instances/db1/storage')
await openActionMenu(page)
// running: disk actions not available
await expect(page.getByRole('option', { name: 'Attach existing disk' })).toBeHidden()
await expect(page.getByRole('option', { name: 'Create disk' })).toBeHidden()
await page.keyboard.press('Escape')

await stopInstance(page)
await openActionMenu(page)
await expect(page.getByRole('option', { name: 'Attach existing disk' })).toBeVisible()
await expect(page.getByRole('option', { name: 'Create disk' })).toBeVisible()
})

test('networking tab quick actions: NIC gated by run state, IPs/subnets by availability', async ({
page,
}) => {
await page.goto('/projects/mock-project/instances/db1')
await page.getByRole('tab', { name: 'Networking' }).click()
await openActionMenu(page)
// running: NIC creation unavailable
await expect(page.getByRole('option', { name: 'Add network interface' })).toBeHidden()
// floating IP, ephemeral IP, and external subnet all have available resources
await expect(page.getByRole('option', { name: 'Attach floating IP' })).toBeVisible()
await expect(page.getByRole('option', { name: 'Attach ephemeral IP' })).toBeVisible()
await expect(page.getByRole('option', { name: 'Attach external subnet' })).toBeVisible()
await page.keyboard.press('Escape')

await stopInstance(page)
await page.getByRole('tab', { name: 'Networking' }).click()
await openActionMenu(page)
await expect(page.getByRole('option', { name: 'Add network interface' })).toBeVisible()
})

test('SCIM tab quick action "Create token" visible when permitted, hidden when not', async ({
page,
browser,
}) => {
// default session (fleet admin): action should appear
await page.goto('/system/silos/maze-war/scim')
await openActionMenu(page)
await expect(page.getByRole('option', { name: 'Create token' })).toBeVisible()
await page.keyboard.press('Escape')

// Jane Austen: fleet viewer, not silo admin on maze-war — gets 403, action should not appear
const janeAustenPage = await getPageAsUser(browser, 'Jane Austen')
await janeAustenPage.goto('/system/silos/maze-war/scim')
await openActionMenu(janeAustenPage)
await expect(janeAustenPage.getByRole('option', { name: 'Create token' })).toBeHidden()
})
Loading