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
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"react/button-has-type": "error",
"react/jsx-boolean-value": "error",

"react-hooks/exhaustive-deps": "error",
"react-hooks/exhaustive-deps": ["error", { "additionalHooks": "useQuickActions" }],
"react-hooks/rules-of-hooks": "error",
"import/no-default-export": "error",
"consistent-type-imports": "error",
Expand Down
13 changes: 10 additions & 3 deletions app/hooks/use-quick-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,22 @@ function useParentActions(): QuickActionItem[] {
}

/**
* Register action items with the global quick actions menu. `items` must
* be memoized by the caller, otherwise the effect will run too often.
* Register action items with the global quick actions menu. Takes a factory
* and deps array like useMemo — the linter checks deps via additionalHooks.
*
* Each component instance gets its own source slot (via useId), so items are
* cleaned up automatically when the component unmounts without affecting
* other sources' registrations.
*/
export function useQuickActions(items: QuickActionItem[]) {
export function useQuickActions(
factory: () => QuickActionItem[],
deps: React.DependencyList
) {
const sourceId = useId()
// Deps are checked by the linter at call sites via the additionalHooks
// option in .oxlintrc.json, so we can safely forward them here.
// eslint-disable-next-line react-hooks/exhaustive-deps
const items = useMemo(factory, deps)

useEffect(() => {
setSourceItems(sourceId, items)
Expand Down
43 changes: 21 additions & 22 deletions app/layouts/ProjectLayoutBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* Copyright Oxide Computer Company
*/
import { useMemo, type ReactElement } from 'react'
import type { ReactElement } from 'react'
import { useLocation, type LoaderFunctionArgs } from 'react-router'

import { api, q, queryClient, usePrefetchedQuery } from '@oxide/api'
Expand Down Expand Up @@ -58,28 +58,27 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
const { data: project } = usePrefetchedQuery(projectView(projectSelector))

const { pathname } = useLocation()

useQuickActions(
useMemo(
() =>
[
{ value: 'Instances', path: pb.instances(projectSelector) },
{ value: 'Disks', path: pb.disks(projectSelector) },
{ value: 'Snapshots', path: pb.snapshots(projectSelector) },
{ value: 'Images', path: pb.projectImages(projectSelector) },
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
{ value: 'Affinity Groups', path: pb.affinity(projectSelector) },
{ value: 'Project Access', path: pb.projectAccess(projectSelector) },
]
// filter out the entry for the path we're currently on
.filter((i) => i.path !== pathname)
.map((i) => ({
navGroup: `Project '${project.name}'`,
value: i.value,
action: i.path,
})),
[pathname, project.name, projectSelector]
)
() =>
[
{ value: 'Instances', path: pb.instances(projectSelector) },
{ value: 'Disks', path: pb.disks(projectSelector) },
{ value: 'Snapshots', path: pb.snapshots(projectSelector) },
{ value: 'Images', path: pb.projectImages(projectSelector) },
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
{ value: 'Affinity Groups', path: pb.affinity(projectSelector) },
{ value: 'Project Access', path: pb.projectAccess(projectSelector) },
]
// filter out the entry for the path we're currently on
.filter((i) => i.path !== pathname)
.map((i) => ({
navGroup: `Project '${project.name}'`,
value: i.value,
action: i.path,
})),
[pathname, project.name, projectSelector]
)

return (
Expand Down
31 changes: 14 additions & 17 deletions app/layouts/SettingsLayout.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 { useLocation } from 'react-router'

import {
Expand All @@ -30,22 +29,20 @@ export default function SettingsLayout() {
const { pathname } = useLocation()

useQuickActions(
useMemo(
() =>
[
{ value: 'Profile', path: pb.profile() },
{ value: 'SSH Keys', path: pb.sshKeys() },
{ value: 'Access Tokens', path: pb.accessTokens() },
]
// filter out the entry for the path we're currently on
.filter((i) => i.path !== pathname)
.map((i) => ({
navGroup: `Settings`,
value: i.value,
action: i.path,
})),
[pathname]
)
() =>
[
{ value: 'Profile', path: pb.profile() },
{ value: 'SSH Keys', path: pb.sshKeys() },
{ value: 'Access Tokens', path: pb.accessTokens() },
]
// filter out the entry for the path we're currently on
.filter((i) => i.path !== pathname)
.map((i) => ({
navGroup: `Settings`,
value: i.value,
action: i.path,
})),
[pathname]
)

return (
Expand Down
33 changes: 15 additions & 18 deletions app/layouts/SiloLayout.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 { useLocation } from 'react-router'

import {
Expand All @@ -29,23 +28,21 @@ export default function SiloLayout() {
const { me } = useCurrentUser()

useQuickActions(
useMemo(
() =>
[
{ value: 'Projects', path: pb.projects() },
{ value: 'Images', path: pb.siloImages() },
{ value: 'Utilization', path: pb.siloUtilization() },
{ value: 'Silo Access', path: pb.siloAccess() },
]
// filter out the entry for the path we're currently on
.filter((i) => i.path !== pathname)
.map((i) => ({
navGroup: `Silo '${me.siloName}'`,
value: i.value,
action: i.path,
})),
[pathname, me.siloName]
)
() =>
[
{ value: 'Projects', path: pb.projects() },
{ value: 'Images', path: pb.siloImages() },
{ value: 'Utilization', path: pb.siloUtilization() },
{ value: 'Silo Access', path: pb.siloAccess() },
]
// filter out the entry for the path we're currently on
.filter((i) => i.path !== pathname)
.map((i) => ({
navGroup: `Silo '${me.siloName}'`,
value: i.value,
action: i.path,
})),
[pathname, me.siloName]
)

return (
Expand Down
5 changes: 1 addition & 4 deletions app/layouts/SystemLayout.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 { useLocation } from 'react-router'

import { api, q, queryClient } from '@oxide/api'
Expand Down Expand Up @@ -48,7 +47,7 @@ export default function SystemLayout() {

const { me } = useCurrentUser()

const actions = useMemo(() => {
useQuickActions(() => {
const systemLinks = [
{ value: 'Silos', path: pb.silos() },
{ value: 'Utilization', path: pb.systemUtilization() },
Expand All @@ -73,8 +72,6 @@ export default function SystemLayout() {
return [...systemLinks, backToSilo]
}, [pathname, me.siloName])

useQuickActions(actions)

return (
<PageContainer>
<TopBar systemOrSilo="system" />
Expand Down
31 changes: 15 additions & 16 deletions app/pages/ProjectsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { useQuery } from '@tanstack/react-query'
import { createColumnHelper } from '@tanstack/react-table'
import { useCallback, useMemo } from 'react'
import { useCallback } from 'react'
import { Outlet, useNavigate } from 'react-router'

import { api, getListQFn, q, queryClient, useApiMutation, type Project } from '@oxide/api'
Expand Down Expand Up @@ -103,22 +103,21 @@ export default function ProjectsPage() {
})

const { data: allProjects } = useQuery(q(api.projectList, { query: { limit: ALL_ISH } }))

useQuickActions(
useMemo(
() => [
{
value: 'New project',
navGroup: 'Actions',
action: pb.projectsNew(),
},
...(allProjects?.items || []).map((p) => ({
value: p.name,
action: pb.project({ project: p.name }),
navGroup: 'Go to project',
})),
],
[allProjects]
)
() => [
{
value: 'New project',
navGroup: 'Actions',
action: pb.projectsNew(),
},
...(allProjects?.items || []).map((p) => ({
value: p.name,
action: pb.project({ project: p.name }),
navGroup: 'Go to project',
})),
],
[allProjects]
)

return (
Expand Down
18 changes: 8 additions & 10 deletions app/pages/SiloAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,14 @@ export default function SiloAccessPage() {
})

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

return (
Expand Down
29 changes: 14 additions & 15 deletions app/pages/SiloImagesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,21 @@ export default function SiloImagesPage() {
const { table } = useQueryTable({ query: imageList, columns, emptyState: <EmptyState /> })

const { data: allImages } = useQuery(q(api.imageList, { query: { limit: ALL_ISH } }))

useQuickActions(
useMemo(
() => [
{
value: 'Promote image',
navGroup: 'Actions',
action: () => setShowModal(true),
},
...(allImages?.items || []).map((i) => ({
value: i.name,
action: pb.siloImageEdit({ image: i.name }),
navGroup: 'Go to silo image',
})),
],
[allImages]
)
() => [
{
value: 'Promote image',
navGroup: 'Actions',
action: () => setShowModal(true),
},
...(allImages?.items || []).map((i) => ({
value: i.name,
action: pb.siloImageEdit({ image: i.name }),
navGroup: 'Go to silo image',
})),
],
[allImages]
)

return (
Expand Down
18 changes: 8 additions & 10 deletions app/pages/project/access/ProjectAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,16 +207,14 @@ export default function ProjectAccessPage() {
})

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

return (
Expand Down
30 changes: 14 additions & 16 deletions app/pages/project/affinity/AffinityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

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

import {
Expand Down Expand Up @@ -140,21 +140,19 @@ export default function AffinityPage() {
})

useQuickActions(
useMemo(
() => [
{
value: 'New anti-affinity group',
navGroup: 'Actions',
action: pb.affinityNew({ project }),
},
...antiAffinityGroups.map((g) => ({
value: g.name,
action: pb.antiAffinityGroup({ project, antiAffinityGroup: g.name }),
navGroup: 'Go to anti-affinity group',
})),
],
[project, antiAffinityGroups]
)
() => [
{
value: 'New anti-affinity group',
navGroup: 'Actions',
action: pb.affinityNew({ project }),
},
...antiAffinityGroups.map((g) => ({
value: g.name,
action: pb.antiAffinityGroup({ project, antiAffinityGroup: g.name }),
navGroup: 'Go to anti-affinity group',
})),
],
[project, antiAffinityGroups]
)

return (
Expand Down
Loading
Loading