Skip to content

Commit 27cf679

Browse files
authored
Allow links in row actions and more actions dropdowns (#2751)
* allow links in more actions dropdowns, refactor * fix bug claude found * add ext link icon in link items * about metric -> about this metric
1 parent 1cfb3b2 commit 27cf679

File tree

15 files changed

+172
-181
lines changed

15 files changed

+172
-181
lines changed

app/components/CopyIdItem.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
9+
import * as DropdownMenu from '~/ui/lib/DropdownMenu'
10+
11+
export function CopyIdItem({ id, label = 'Copy ID' }: { id: string; label?: string }) {
12+
return (
13+
<DropdownMenu.Item
14+
onSelect={() => window.navigator.clipboard.writeText(id)}
15+
label={label}
16+
/>
17+
)
18+
}

app/components/MoreActionsMenu.tsx

+6-18
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,24 @@
66
* Copyright Oxide Computer Company
77
*/
88
import cn from 'classnames'
9+
import { type ReactNode } from 'react'
910

1011
import { More12Icon } from '@oxide/design-system/icons/react'
1112

12-
import type { MenuAction } from '~/table/columns/action-col'
1313
import * as DropdownMenu from '~/ui/lib/DropdownMenu'
14-
import { Tooltip } from '~/ui/lib/Tooltip'
15-
import { Wrap } from '~/ui/util/wrap'
1614

1715
interface MoreActionsMenuProps {
1816
/** The accessible name for the menu button */
1917
label: string
20-
actions: MenuAction[]
2118
isSmall?: boolean
19+
/** Dropdown items only */
20+
children?: ReactNode
2221
}
22+
2323
export const MoreActionsMenu = ({
24-
actions,
2524
label,
2625
isSmall = false,
26+
children,
2727
}: MoreActionsMenuProps) => {
2828
return (
2929
<DropdownMenu.Root>
@@ -36,19 +36,7 @@ export const MoreActionsMenu = ({
3636
>
3737
<More12Icon />
3838
</DropdownMenu.Trigger>
39-
<DropdownMenu.Content className="mt-2">
40-
{actions.map((a) => (
41-
<Wrap key={a.label} when={!!a.disabled} with={<Tooltip content={a.disabled} />}>
42-
<DropdownMenu.Item
43-
className={a.className}
44-
disabled={!!a.disabled}
45-
onSelect={a.onActivate}
46-
>
47-
{a.label}
48-
</DropdownMenu.Item>
49-
</Wrap>
50-
))}
51-
</DropdownMenu.Content>
39+
<DropdownMenu.Content className="mt-2">{children}</DropdownMenu.Content>
5240
</DropdownMenu.Root>
5341
)
5442
}

app/components/TopBar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ function UserMenu() {
146146
</DropdownMenu.Trigger>
147147
<DropdownMenu.Content gap={8}>
148148
<DropdownMenu.LinkItem to={pb.profile()}>Settings</DropdownMenu.LinkItem>
149-
<DropdownMenu.Item onSelect={() => logout.mutate({})}>Sign out</DropdownMenu.Item>
149+
<DropdownMenu.Item onSelect={() => logout.mutate({})} label="Sign out" />
150150
</DropdownMenu.Content>
151151
</DropdownMenu.Root>
152152
)

app/components/oxql-metrics/OxqlMetric.tsx

+7-17
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { MoreActionsMenu } from '~/components/MoreActionsMenu'
2121
import { getInstanceSelector } from '~/hooks/use-params'
2222
import { useMetricsContext } from '~/pages/project/instances/common'
2323
import { LearnMore } from '~/ui/lib/CardBlock'
24+
import * as Dropdown from '~/ui/lib/DropdownMenu'
2425
import { classed } from '~/util/classed'
2526
import { links } from '~/util/links'
2627

@@ -85,23 +86,12 @@ export function OxqlMetric({ title, description, unit, ...queryObj }: OxqlMetric
8586
</h2>
8687
<div className="mt-0.5 text-sans-md text-secondary">{description}</div>
8788
</div>
88-
<MoreActionsMenu
89-
label="Instance actions"
90-
actions={[
91-
{
92-
label: 'About metric',
93-
onActivate: () => {
94-
const url = links.oxqlSchemaDocs(queryObj.metricName)
95-
window.open(url, '_blank', 'noopener,noreferrer')
96-
},
97-
},
98-
{
99-
label: 'View OxQL query',
100-
onActivate: () => setModalOpen(true),
101-
},
102-
]}
103-
isSmall
104-
/>
89+
<MoreActionsMenu label="Instance actions" isSmall>
90+
<Dropdown.LinkItem to={links.oxqlSchemaDocs(queryObj.metricName)}>
91+
About this metric
92+
</Dropdown.LinkItem>
93+
<Dropdown.Item onSelect={() => setModalOpen(true)} label="View OxQL query" />
94+
</MoreActionsMenu>
10595
<CopyCodeModal
10696
isOpen={modalOpen}
10797
onDismiss={() => setModalOpen(false)}

app/pages/project/instances/InstancePage.tsx

+25-15
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Copyright Oxide Computer Company
77
*/
88
import { filesize } from 'filesize'
9-
import { useId, useMemo, useState } from 'react'
9+
import { useId, useState } from 'react'
1010
import { useForm } from 'react-hook-form'
1111
import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router'
1212

@@ -27,6 +27,7 @@ import {
2727
instanceCan,
2828
instanceTransitioning,
2929
} from '~/api/util'
30+
import { CopyIdItem } from '~/components/CopyIdItem'
3031
import { ExternalIps } from '~/components/ExternalIps'
3132
import { NumberField } from '~/components/form/fields/NumberField'
3233
import { HL } from '~/components/HL'
@@ -44,6 +45,7 @@ import {
4445
import { addToast } from '~/stores/toast'
4546
import { EmptyCell } from '~/table/cells/EmptyCell'
4647
import { Button } from '~/ui/lib/Button'
48+
import * as DropdownMenu from '~/ui/lib/DropdownMenu'
4749
import { Message } from '~/ui/lib/Message'
4850
import { Modal } from '~/ui/lib/Modal'
4951
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
@@ -179,19 +181,6 @@ export default function InstancePage() {
179181
{ enabled: !!primaryVpcId }
180182
)
181183

182-
const allMenuActions = useMemo(
183-
() => [
184-
{
185-
label: 'Copy ID',
186-
onActivate() {
187-
window.navigator.clipboard.writeText(instance.id || '')
188-
},
189-
},
190-
...makeMenuActions(instance),
191-
],
192-
[instance, makeMenuActions]
193-
)
194-
195184
const memory = filesize(instance.memory, { output: 'object', base: 2 })
196185

197186
return (
@@ -215,7 +204,28 @@ export default function InstancePage() {
215204
</Button>
216205
))}
217206
</div>
218-
<MoreActionsMenu label="Instance actions" actions={allMenuActions} />
207+
<MoreActionsMenu label="Instance actions">
208+
<CopyIdItem id={instance.id} />
209+
{makeMenuActions(instance).map((action) =>
210+
'to' in action ? (
211+
<DropdownMenu.LinkItem
212+
key={action.label}
213+
to={action.to}
214+
className={action.className}
215+
>
216+
{action.label}
217+
</DropdownMenu.LinkItem>
218+
) : (
219+
<DropdownMenu.Item
220+
key={action.label}
221+
label={action.label}
222+
onSelect={action.onActivate}
223+
disabled={action.disabled}
224+
className={action.className}
225+
/>
226+
)
227+
)}
228+
</MoreActionsMenu>
219229
</div>
220230
</PageHeader>
221231
<PropertiesTable columns={2} className="-mt-8 mb-8">

app/pages/project/instances/actions.tsx

+6-8
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
* Copyright Oxide Computer Company
77
*/
88
import { useCallback } from 'react'
9-
import { useNavigate } from 'react-router'
109

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

1312
import { HL } from '~/components/HL'
1413
import { confirmAction } from '~/stores/confirm-action'
1514
import { confirmDelete } from '~/stores/confirm-delete'
1615
import { addToast } from '~/stores/toast'
16+
import type { MenuAction, MenuActionItem } from '~/table/columns/action-col'
1717
import { pb } from '~/util/path-builder'
1818

1919
import { fancifyStates } from './common'
@@ -50,7 +50,8 @@ export const useMakeInstanceActions = (
5050
const { onResizeClick } = options
5151

5252
const makeButtonActions = useCallback(
53-
(instance: Instance) => {
53+
// restrict to items for now so we don't have to handle links in the calling code
54+
(instance: Instance): MenuActionItem[] => {
5455
const instanceParams = { path: { instance: instance.name }, query: { project } }
5556
return [
5657
{
@@ -116,9 +117,8 @@ export const useMakeInstanceActions = (
116117
[project, startInstanceAsync, stopInstanceAsync]
117118
)
118119

119-
const navigate = useNavigate()
120120
const makeMenuActions = useCallback(
121-
(instance: Instance) => {
121+
(instance: Instance): MenuAction[] => {
122122
const instanceParams = { path: { instance: instance.name }, query: { project } }
123123
return [
124124
{
@@ -153,9 +153,7 @@ export const useMakeInstanceActions = (
153153
},
154154
{
155155
label: 'View serial console',
156-
onActivate() {
157-
navigate(pb.serialConsole({ project, instance: instance.name }))
158-
},
156+
to: pb.serialConsole({ project, instance: instance.name }),
159157
},
160158
{
161159
label: 'Delete',
@@ -179,7 +177,7 @@ export const useMakeInstanceActions = (
179177
// Do not put `options` in here, refer to the property. options is not ref
180178
// stable. Extra renders here cause the row actions menu to close when it
181179
// shouldn't, like during polling on instance list.
182-
[project, deleteInstanceAsync, rebootInstanceAsync, onResizeClick, navigate]
180+
[project, deleteInstanceAsync, rebootInstanceAsync, onResizeClick]
183181
)
184182

185183
return { makeButtonActions, makeMenuActions }

app/pages/project/vpcs/RouterPage.tsx

+5-14
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { createColumnHelper } from '@tanstack/react-table'
10-
import { useCallback, useMemo } from 'react'
10+
import { useCallback } from 'react'
1111
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
1212

1313
import { Networking16Icon, Networking24Icon } from '@oxide/design-system/icons/react'
@@ -23,6 +23,7 @@ import {
2323
type RouterRoute,
2424
type RouteTarget,
2525
} from '~/api'
26+
import { CopyIdItem } from '~/components/CopyIdItem'
2627
import { DocsPopover } from '~/components/DocsPopover'
2728
import { HL } from '~/components/HL'
2829
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
@@ -97,18 +98,6 @@ export default function RouterPage() {
9798
},
9899
})
99100

100-
const actions = useMemo(
101-
() => [
102-
{
103-
label: 'Copy ID',
104-
onActivate() {
105-
window.navigator.clipboard.writeText(routerData.id || '')
106-
},
107-
},
108-
],
109-
[routerData]
110-
)
111-
112101
const emptyState = (
113102
<EmptyMessage
114103
icon={<Networking24Icon />}
@@ -197,7 +186,9 @@ export default function RouterPage() {
197186
summary="Routers are collections of routes that direct traffic between VPCs and their subnets."
198187
links={[docLinks.routers]}
199188
/>
200-
<MoreActionsMenu label="Router actions" actions={actions} />
189+
<MoreActionsMenu label="Router actions">
190+
<CopyIdItem id={routerData.id} />
191+
</MoreActionsMenu>
201192
</div>
202193
</PageHeader>
203194
<PropertiesTable columns={2} className="-mt-8 mb-8">

app/pages/project/vpcs/VpcPage.tsx

+12-22
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { useMemo } from 'react'
98
import { useNavigate, type LoaderFunctionArgs } from 'react-router'
109

1110
import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api'
@@ -17,6 +16,7 @@ import { RouteTabs, Tab } from '~/components/RouteTabs'
1716
import { getVpcSelector, useVpcSelector } from '~/hooks/use-params'
1817
import { confirmDelete } from '~/stores/confirm-delete'
1918
import { addToast } from '~/stores/toast'
19+
import * as DropdownMenu from '~/ui/lib/DropdownMenu'
2020
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
2121
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
2222
import { pb } from '~/util/path-builder'
@@ -46,33 +46,23 @@ export default function VpcPage() {
4646
},
4747
})
4848

49-
const actions = useMemo(
50-
() => [
51-
{
52-
label: 'Edit',
53-
onActivate() {
54-
navigate(pb.vpcEdit(vpcSelector))
55-
},
56-
},
57-
{
58-
label: 'Delete',
59-
onActivate: confirmDelete({
60-
doDelete: () => deleteVpc({ path: { vpc: vpcName }, query: { project } }),
61-
label: vpcName,
62-
}),
63-
className: 'destructive',
64-
},
65-
],
66-
[deleteVpc, navigate, project, vpcName, vpcSelector]
67-
)
68-
6949
return (
7050
<>
7151
<PageHeader>
7252
<PageTitle icon={<Networking24Icon />}>{vpc.name}</PageTitle>
7353
<div className="inline-flex gap-2">
7454
<VpcDocsPopover />
75-
<MoreActionsMenu label="VPC actions" actions={actions} />
55+
<MoreActionsMenu label="VPC actions">
56+
<DropdownMenu.LinkItem to={pb.vpcEdit(vpcSelector)}>Edit</DropdownMenu.LinkItem>
57+
<DropdownMenu.Item
58+
label="Delete"
59+
onSelect={confirmDelete({
60+
doDelete: () => deleteVpc({ path: { vpc: vpcName }, query: { project } }),
61+
label: vpcName,
62+
})}
63+
className="destructive"
64+
/>
65+
</MoreActionsMenu>
7666
</div>
7767
</PageHeader>
7868
<PropertiesTable columns={2} className="-mt-8 mb-8">

app/pages/project/vpcs/VpcSubnetsTab.tsx

+3-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import { createColumnHelper } from '@tanstack/react-table'
99
import { useCallback, useMemo } from 'react'
10-
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
10+
import { Outlet, type LoaderFunctionArgs } from 'react-router'
1111

1212
import {
1313
getListQFn,
@@ -55,14 +55,11 @@ export default function VpcSubnetsTab() {
5555
},
5656
})
5757

58-
const navigate = useNavigate()
59-
6058
const makeActions = useCallback(
6159
(subnet: VpcSubnet): MenuAction[] => [
6260
{
6361
label: 'Edit',
64-
onActivate: () =>
65-
navigate(pb.vpcSubnetsEdit({ ...vpcSelector, subnet: subnet.name })),
62+
to: pb.vpcSubnetsEdit({ ...vpcSelector, subnet: subnet.name }),
6663
},
6764
// TODO: only show if you have permission to do this
6865
{
@@ -73,7 +70,7 @@ export default function VpcSubnetsTab() {
7370
}),
7471
},
7572
],
76-
[navigate, deleteSubnet, vpcSelector]
73+
[deleteSubnet, vpcSelector]
7774
)
7875

7976
const columns = useMemo(

0 commit comments

Comments
 (0)