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
18 changes: 18 additions & 0 deletions app/components/CopyIdItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/

import * as DropdownMenu from '~/ui/lib/DropdownMenu'

export function CopyIdItem({ id, label = 'Copy ID' }: { id: string; label?: string }) {
return (
<DropdownMenu.Item
onSelect={() => window.navigator.clipboard.writeText(id)}
label={label}
/>
)
}
24 changes: 6 additions & 18 deletions app/components/MoreActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import { type ReactNode } from 'react'

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

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

interface MoreActionsMenuProps {
/** The accessible name for the menu button */
label: string
actions: MenuAction[]
isSmall?: boolean
/** Dropdown items only */
children?: ReactNode
}

export const MoreActionsMenu = ({
actions,
label,
isSmall = false,
children,
}: MoreActionsMenuProps) => {
return (
<DropdownMenu.Root>
Expand All @@ -36,19 +36,7 @@ export const MoreActionsMenu = ({
>
<More12Icon />
</DropdownMenu.Trigger>
<DropdownMenu.Content className="mt-2">
{actions.map((a) => (
<Wrap key={a.label} when={!!a.disabled} with={<Tooltip content={a.disabled} />}>
<DropdownMenu.Item
className={a.className}
disabled={!!a.disabled}
onSelect={a.onActivate}
>
{a.label}
</DropdownMenu.Item>
</Wrap>
))}
</DropdownMenu.Content>
<DropdownMenu.Content className="mt-2">{children}</DropdownMenu.Content>
</DropdownMenu.Root>
)
}
2 changes: 1 addition & 1 deletion app/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ function UserMenu() {
</DropdownMenu.Trigger>
<DropdownMenu.Content gap={8}>
<DropdownMenu.LinkItem to={pb.profile()}>Settings</DropdownMenu.LinkItem>
<DropdownMenu.Item onSelect={() => logout.mutate({})}>Sign out</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => logout.mutate({})} label="Sign out" />
</DropdownMenu.Content>
</DropdownMenu.Root>
)
Expand Down
24 changes: 7 additions & 17 deletions app/components/oxql-metrics/OxqlMetric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import { getInstanceSelector } from '~/hooks/use-params'
import { useMetricsContext } from '~/pages/project/instances/common'
import { LearnMore } from '~/ui/lib/CardBlock'
import * as Dropdown from '~/ui/lib/DropdownMenu'
import { classed } from '~/util/classed'
import { links } from '~/util/links'

Expand Down Expand Up @@ -85,23 +86,12 @@ export function OxqlMetric({ title, description, unit, ...queryObj }: OxqlMetric
</h2>
<div className="mt-0.5 text-sans-md text-secondary">{description}</div>
</div>
<MoreActionsMenu
label="Instance actions"
actions={[
{
label: 'About metric',
onActivate: () => {
const url = links.oxqlSchemaDocs(queryObj.metricName)
window.open(url, '_blank', 'noopener,noreferrer')
},
},
{
label: 'View OxQL query',
onActivate: () => setModalOpen(true),
},
]}
isSmall
/>
<MoreActionsMenu label="Instance actions" isSmall>
<Dropdown.LinkItem to={links.oxqlSchemaDocs(queryObj.metricName)}>
About this metric
</Dropdown.LinkItem>
<Dropdown.Item onSelect={() => setModalOpen(true)} label="View OxQL query" />
</MoreActionsMenu>
<CopyCodeModal
isOpen={modalOpen}
onDismiss={() => setModalOpen(false)}
Expand Down
40 changes: 25 additions & 15 deletions app/pages/project/instances/InstancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Copyright Oxide Computer Company
*/
import { filesize } from 'filesize'
import { useId, useMemo, useState } from 'react'
import { useId, useState } from 'react'
import { useForm } from 'react-hook-form'
import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router'

Expand All @@ -27,6 +27,7 @@ import {
instanceCan,
instanceTransitioning,
} from '~/api/util'
import { CopyIdItem } from '~/components/CopyIdItem'
import { ExternalIps } from '~/components/ExternalIps'
import { NumberField } from '~/components/form/fields/NumberField'
import { HL } from '~/components/HL'
Expand All @@ -44,6 +45,7 @@ import {
import { addToast } from '~/stores/toast'
import { EmptyCell } from '~/table/cells/EmptyCell'
import { Button } from '~/ui/lib/Button'
import * as DropdownMenu from '~/ui/lib/DropdownMenu'
import { Message } from '~/ui/lib/Message'
import { Modal } from '~/ui/lib/Modal'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
Expand Down Expand Up @@ -179,19 +181,6 @@ export default function InstancePage() {
{ enabled: !!primaryVpcId }
)

const allMenuActions = useMemo(
() => [
{
label: 'Copy ID',
onActivate() {
window.navigator.clipboard.writeText(instance.id || '')
},
},
...makeMenuActions(instance),
],
[instance, makeMenuActions]
)

const memory = filesize(instance.memory, { output: 'object', base: 2 })

return (
Expand All @@ -215,7 +204,28 @@ export default function InstancePage() {
</Button>
))}
</div>
<MoreActionsMenu label="Instance actions" actions={allMenuActions} />
<MoreActionsMenu label="Instance actions">
<CopyIdItem id={instance.id} />
{makeMenuActions(instance).map((action) =>
'to' in action ? (
<DropdownMenu.LinkItem
key={action.label}
to={action.to}
className={action.className}
>
{action.label}
</DropdownMenu.LinkItem>
) : (
<DropdownMenu.Item
key={action.label}
label={action.label}
onSelect={action.onActivate}
disabled={action.disabled}
className={action.className}
/>
)
)}
</MoreActionsMenu>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the one spot where we are adding some noise, because this is the one spot where we were passing the same set of action config objects to RowActions in one place (instance list) and MoreActionsMenu in another (here, instance detail). Because MoreActionsMenu has been converted to take children directly, we have to do the conversion to elements here. Maybe this should be extracted since it's the same as what RowActions does internally....

</div>
</PageHeader>
<PropertiesTable columns={2} className="-mt-8 mb-8">
Expand Down
14 changes: 6 additions & 8 deletions app/pages/project/instances/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
* Copyright Oxide Computer Company
*/
import { useCallback } from 'react'
import { useNavigate } from 'react-router'

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 { MenuAction, MenuActionItem } from '~/table/columns/action-col'
import { pb } from '~/util/path-builder'

import { fancifyStates } from './common'
Expand Down Expand Up @@ -50,7 +50,8 @@ export const useMakeInstanceActions = (
const { onResizeClick } = options

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

const navigate = useNavigate()
const makeMenuActions = useCallback(
(instance: Instance) => {
(instance: Instance): MenuAction[] => {
const instanceParams = { path: { instance: instance.name }, query: { project } }
return [
{
Expand Down Expand Up @@ -153,9 +153,7 @@ export const useMakeInstanceActions = (
},
{
label: 'View serial console',
onActivate() {
navigate(pb.serialConsole({ project, instance: instance.name }))
},
to: pb.serialConsole({ project, instance: instance.name }),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the narrow change asked for on behalf of an external user in #1855. This allows serial console to be opened in a new tab from both instance list row actions and instance detail "more actions".

},
{
label: 'Delete',
Expand All @@ -179,7 +177,7 @@ export const useMakeInstanceActions = (
// Do not put `options` in here, refer to the property. options is not ref
// stable. Extra renders here cause the row actions menu to close when it
// shouldn't, like during polling on instance list.
[project, deleteInstanceAsync, rebootInstanceAsync, onResizeClick, navigate]
[project, deleteInstanceAsync, rebootInstanceAsync, onResizeClick]
)

return { makeButtonActions, makeMenuActions }
Expand Down
19 changes: 5 additions & 14 deletions app/pages/project/vpcs/RouterPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

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

import { Networking16Icon, Networking24Icon } from '@oxide/design-system/icons/react'
Expand All @@ -23,6 +23,7 @@ import {
type RouterRoute,
type RouteTarget,
} from '~/api'
import { CopyIdItem } from '~/components/CopyIdItem'
import { DocsPopover } from '~/components/DocsPopover'
import { HL } from '~/components/HL'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
Expand Down Expand Up @@ -97,18 +98,6 @@ export default function RouterPage() {
},
})

const actions = useMemo(
() => [
{
label: 'Copy ID',
onActivate() {
window.navigator.clipboard.writeText(routerData.id || '')
},
},
],
[routerData]
)

const emptyState = (
<EmptyMessage
icon={<Networking24Icon />}
Expand Down Expand Up @@ -197,7 +186,9 @@ export default function RouterPage() {
summary="Routers are collections of routes that direct traffic between VPCs and their subnets."
links={[docLinks.routers]}
/>
<MoreActionsMenu label="Router actions" actions={actions} />
<MoreActionsMenu label="Router actions">
<CopyIdItem id={routerData.id} />
</MoreActionsMenu>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

net negative lines speak for themselves I think

</div>
</PageHeader>
<PropertiesTable columns={2} className="-mt-8 mb-8">
Expand Down
34 changes: 12 additions & 22 deletions app/pages/project/vpcs/VpcPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*
* Copyright Oxide Computer Company
*/
import { useMemo } from 'react'
import { useNavigate, type LoaderFunctionArgs } from 'react-router'

import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api'
Expand All @@ -17,6 +16,7 @@ import { RouteTabs, Tab } from '~/components/RouteTabs'
import { getVpcSelector, useVpcSelector } from '~/hooks/use-params'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import * as DropdownMenu from '~/ui/lib/DropdownMenu'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { pb } from '~/util/path-builder'
Expand Down Expand Up @@ -46,33 +46,23 @@ export default function VpcPage() {
},
})

const actions = useMemo(
() => [
{
label: 'Edit',
onActivate() {
navigate(pb.vpcEdit(vpcSelector))
},
},
{
label: 'Delete',
onActivate: confirmDelete({
doDelete: () => deleteVpc({ path: { vpc: vpcName }, query: { project } }),
label: vpcName,
}),
className: 'destructive',
},
],
[deleteVpc, navigate, project, vpcName, vpcSelector]
)

return (
<>
<PageHeader>
<PageTitle icon={<Networking24Icon />}>{vpc.name}</PageTitle>
<div className="inline-flex gap-2">
<VpcDocsPopover />
<MoreActionsMenu label="VPC actions" actions={actions} />
<MoreActionsMenu label="VPC actions">
<DropdownMenu.LinkItem to={pb.vpcEdit(vpcSelector)}>Edit</DropdownMenu.LinkItem>
<DropdownMenu.Item
label="Delete"
onSelect={confirmDelete({
doDelete: () => deleteVpc({ path: { vpc: vpcName }, query: { project } }),
label: vpcName,
})}
className="destructive"
/>
</MoreActionsMenu>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This shows why the children approach is nice. There's no config object where you then elsewhere have to decide what it renders. You just render those things directly.

</div>
</PageHeader>
<PropertiesTable columns={2} className="-mt-8 mb-8">
Expand Down
9 changes: 3 additions & 6 deletions app/pages/project/vpcs/VpcSubnetsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { createColumnHelper } from '@tanstack/react-table'
import { useCallback, useMemo } from 'react'
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
import { Outlet, type LoaderFunctionArgs } from 'react-router'

import {
getListQFn,
Expand Down Expand Up @@ -55,14 +55,11 @@ export default function VpcSubnetsTab() {
},
})

const navigate = useNavigate()

const makeActions = useCallback(
(subnet: VpcSubnet): MenuAction[] => [
{
label: 'Edit',
onActivate: () =>
navigate(pb.vpcSubnetsEdit({ ...vpcSelector, subnet: subnet.name })),
to: pb.vpcSubnetsEdit({ ...vpcSelector, subnet: subnet.name }),
},
// TODO: only show if you have permission to do this
{
Expand All @@ -73,7 +70,7 @@ export default function VpcSubnetsTab() {
}),
},
],
[navigate, deleteSubnet, vpcSelector]
[deleteSubnet, vpcSelector]
)

const columns = useMemo(
Expand Down
Loading
Loading