diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index 2e351e0c4..f55030d8b 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -5,6 +5,7 @@ * * Copyright Oxide Computer Company */ +import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo } from 'react' import { Outlet, useNavigate } from 'react-router' @@ -24,6 +25,7 @@ import { CreateLink } from '~/ui/lib/CreateButton' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions } from '~/ui/lib/Table' +import { ALL_ISH } from '~/util/consts' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' @@ -94,11 +96,13 @@ export default function ProjectsPage() { ) const columns = useColsWithActions(staticCols, makeActions) - const { - table, - query: { data: projects }, - } = useQueryTable({ query: projectList, columns, emptyState: }) + const { table } = useQueryTable({ + query: projectList, + columns, + emptyState: , + }) + const { data: allProjects } = useQuery(q(api.projectList, { query: { limit: ALL_ISH } })) useQuickActions( useMemo( () => [ @@ -106,13 +110,13 @@ export default function ProjectsPage() { value: 'New project', onSelect: () => navigate(pb.projectsNew()), }, - ...(projects?.items || []).map((p) => ({ + ...(allProjects?.items || []).map((p) => ({ value: p.name, onSelect: () => navigate(pb.project({ project: p.name })), navGroup: 'Go to project', })), ], - [navigate, projects] + [navigate, allProjects] ) ) diff --git a/app/pages/SiloAccessPage.tsx b/app/pages/SiloAccessPage.tsx index 6b274f91b..87ae0a5c2 100644 --- a/app/pages/SiloAccessPage.tsx +++ b/app/pages/SiloAccessPage.tsx @@ -30,6 +30,7 @@ import { SiloAccessEditUserSideModal, } from '~/forms/silo-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' @@ -164,6 +165,18 @@ export default function SiloAccessPage() { getCoreRowModel: getCoreRowModel(), }) + useQuickActions( + useMemo( + () => [ + { + value: 'Add user or group', + onSelect: () => setAddModalOpen(true), + }, + ], + [] + ) + ) + return ( <> diff --git a/app/pages/SiloImagesPage.tsx b/app/pages/SiloImagesPage.tsx index e395d4e44..7cc6f5e53 100644 --- a/app/pages/SiloImagesPage.tsx +++ b/app/pages/SiloImagesPage.tsx @@ -9,7 +9,7 @@ import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' -import { Outlet } from 'react-router' +import { Outlet, useNavigate } from 'react-router' import { api, getListQFn, q, queryClient, useApiMutation, type Image } from '@oxide/api' import { Images16Icon, Images24Icon } from '@oxide/design-system/icons/react' @@ -20,6 +20,7 @@ import { toImageComboboxItem } from '~/components/form/fields/ImageSelectField' import { ListboxField } from '~/components/form/fields/ListboxField' import { ModalForm } from '~/components/form/ModalForm' import { HL } from '~/components/HL' +import { useQuickActions } from '~/hooks/use-quick-actions' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' import { makeLinkCell } from '~/table/cells/LinkCell' @@ -32,6 +33,7 @@ import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { Message } from '~/ui/lib/Message' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions } from '~/ui/lib/Table' +import { ALL_ISH } from '~/util/consts' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' @@ -93,6 +95,26 @@ export default function SiloImagesPage() { const columns = useColsWithActions(staticCols, makeActions) const { table } = useQueryTable({ query: imageList, columns, emptyState: }) + + const navigate = useNavigate() + const { data: allImages } = useQuery(q(api.imageList, { query: { limit: ALL_ISH } })) + useQuickActions( + useMemo( + () => [ + { + value: 'Promote image', + onSelect: () => setShowModal(true), + }, + ...(allImages?.items || []).map((i) => ({ + value: i.name, + onSelect: () => navigate(pb.siloImageEdit({ image: i.name })), + navGroup: 'Go to silo image', + })), + ], + [navigate, allImages] + ) + ) + return ( <> diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 460bf3aae..254144480 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -35,6 +35,7 @@ import { ProjectAccessEditUserSideModal, } from '~/forms/project-access' import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' +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' @@ -205,6 +206,18 @@ export default function ProjectAccessPage() { getCoreRowModel: getCoreRowModel(), }) + useQuickActions( + useMemo( + () => [ + { + value: 'Add user or group', + onSelect: () => setAddModalOpen(true), + }, + ], + [] + ) + ) + return ( <> diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx index 941407de6..8a5adcf92 100644 --- a/app/pages/project/affinity/AffinityPage.tsx +++ b/app/pages/project/affinity/AffinityPage.tsx @@ -7,8 +7,8 @@ */ import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table' -import { useCallback } from 'react' -import { Outlet, type LoaderFunctionArgs } from 'react-router' +import { useCallback, useMemo } from 'react' +import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router' import { api, @@ -25,6 +25,7 @@ import { AffinityDocsPopover, AffinityPolicyHeader } from '~/components/Affinity import { HL } from '~/components/HL' import { antiAffinityGroupList, antiAffinityGroupMemberList } from '~/forms/affinity-util' import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' +import { useQuickActions } from '~/hooks/use-quick-actions' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell' @@ -138,6 +139,19 @@ export default function AffinityPage() { getCoreRowModel: getCoreRowModel(), }) + const navigate = useNavigate() + useQuickActions( + useMemo( + () => [ + { + value: 'New group', + onSelect: () => navigate(pb.affinityNew({ project })), + }, + ], + [navigate, project] + ) + ) + return ( <> diff --git a/app/pages/project/disks/DisksPage.tsx b/app/pages/project/disks/DisksPage.tsx index 05908d411..0acbe56d7 100644 --- a/app/pages/project/disks/DisksPage.tsx +++ b/app/pages/project/disks/DisksPage.tsx @@ -7,7 +7,7 @@ */ import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo } from 'react' -import { Outlet, type LoaderFunctionArgs } from 'react-router' +import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router' import { api, @@ -26,6 +26,7 @@ import { HL } from '~/components/HL' import { DiskStateBadge, DiskTypeBadge, ReadOnlyBadge } from '~/components/StateBadge' import { makeCrumb } from '~/hooks/use-crumbs' import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' +import { useQuickActions } from '~/hooks/use-quick-actions' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' import { InstanceLinkCell } from '~/table/cells/InstanceLinkCell' @@ -186,6 +187,19 @@ export default function DisksPage() { emptyState: , }) + const navigate = useNavigate() + useQuickActions( + useMemo( + () => [ + { + value: 'New disk', + onSelect: () => navigate(pb.disksNew({ project })), + }, + ], + [navigate, project] + ) + ) + return ( <> diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx index 1b0958c68..120a73a28 100644 --- a/app/pages/project/floating-ips/FloatingIpsPage.tsx +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -5,8 +5,9 @@ * * Copyright Oxide Computer Company */ +import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router' @@ -28,6 +29,7 @@ import { ModalForm } from '~/components/form/ModalForm' import { HL } from '~/components/HL' import { makeCrumb } from '~/hooks/use-crumbs' import { getProjectSelector, 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' @@ -211,6 +213,26 @@ export default function FloatingIpsPage() { emptyState: , }) + const { data: allFips } = useQuery( + q(api.floatingIpList, { query: { project, limit: ALL_ISH } }) + ) + useQuickActions( + useMemo( + () => [ + { + value: 'New floating IP', + onSelect: () => navigate(pb.floatingIpsNew({ project })), + }, + ...(allFips?.items || []).map((f) => ({ + value: f.name, + onSelect: () => navigate(pb.floatingIpEdit({ project, floatingIp: f.name })), + navGroup: 'Go to floating IP', + })), + ], + [project, navigate, allFips] + ) + ) + return ( <> diff --git a/app/pages/project/images/ImagesPage.tsx b/app/pages/project/images/ImagesPage.tsx index cd0d559d0..852fa60a0 100644 --- a/app/pages/project/images/ImagesPage.tsx +++ b/app/pages/project/images/ImagesPage.tsx @@ -5,17 +5,19 @@ * * Copyright Oxide Computer Company */ +import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo, useState } from 'react' -import { Outlet, type LoaderFunctionArgs } from 'react-router' +import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router' -import { api, getListQFn, queryClient, useApiMutation, type Image } from '@oxide/api' +import { api, getListQFn, q, queryClient, useApiMutation, type Image } from '@oxide/api' import { Images16Icon, Images24Icon } from '@oxide/design-system/icons/react' import { DocsPopover } from '~/components/DocsPopover' import { HL } from '~/components/HL' import { makeCrumb } from '~/hooks/use-crumbs' import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' +import { useQuickActions } from '~/hooks/use-quick-actions' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' import { makeLinkCell } from '~/table/cells/LinkCell' @@ -28,6 +30,7 @@ import { Message } from '~/ui/lib/Message' import { Modal } from '~/ui/lib/Modal' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions } from '~/ui/lib/Table' +import { ALL_ISH } from '~/util/consts' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' @@ -106,6 +109,27 @@ export default function ImagesPage() { emptyState: , }) + const navigate = useNavigate() + const { data: allImages } = useQuery( + q(api.imageList, { query: { project, limit: ALL_ISH } }) + ) + useQuickActions( + useMemo( + () => [ + { + value: 'Upload image', + onSelect: () => navigate(pb.projectImagesNew({ project })), + }, + ...(allImages?.items || []).map((i) => ({ + value: i.name, + onSelect: () => navigate(pb.projectImageEdit({ project, image: i.name })), + navGroup: 'Go to project image', + })), + ], + [project, navigate, allImages] + ) + ) + return ( <> diff --git a/app/pages/project/instances/InstancesPage.tsx b/app/pages/project/instances/InstancesPage.tsx index 9f313cace..9e4b0117b 100644 --- a/app/pages/project/instances/InstancesPage.tsx +++ b/app/pages/project/instances/InstancesPage.tsx @@ -6,6 +6,7 @@ * Copyright Oxide Computer Company */ import { type UseQueryOptions } from '@tanstack/react-query' +import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { filesize } from 'filesize' import { useMemo, useRef, useState } from 'react' @@ -14,6 +15,7 @@ import { useNavigate, type LoaderFunctionArgs } from 'react-router' import { api, getListQFn, + q, queryClient, type ApiError, type Instance, @@ -37,6 +39,7 @@ import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions } from '~/ui/lib/Table' import { Tooltip } from '~/ui/lib/Tooltip' import { setDiff } from '~/util/array' +import { ALL_ISH } from '~/util/consts' import { toLocaleTimeString } from '~/util/date' import { pb } from '~/util/path-builder' import { pluralize } from '~/util/str' @@ -191,8 +194,11 @@ export default function InstancesPage() { emptyState: , }) - const { data: instances, dataUpdatedAt } = query + const { dataUpdatedAt } = query + const { data: allInstances } = useQuery( + q(api.instanceList, { query: { project, limit: ALL_ISH } }) + ) const navigate = useNavigate() useQuickActions( useMemo( @@ -201,13 +207,13 @@ export default function InstancesPage() { value: 'New instance', onSelect: () => navigate(pb.instancesNew({ project })), }, - ...(instances?.items || []).map((i) => ({ + ...(allInstances?.items || []).map((i) => ({ value: i.name, onSelect: () => navigate(pb.instance({ project, instance: i.name })), navGroup: 'Go to instance', })), ], - [project, instances, navigate] + [project, allInstances, navigate] ) ) diff --git a/app/pages/project/snapshots/SnapshotsPage.tsx b/app/pages/project/snapshots/SnapshotsPage.tsx index fbf4c2a57..a9f30404a 100644 --- a/app/pages/project/snapshots/SnapshotsPage.tsx +++ b/app/pages/project/snapshots/SnapshotsPage.tsx @@ -7,7 +7,7 @@ */ import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router' import { @@ -27,6 +27,7 @@ import { DocsPopover } from '~/components/DocsPopover' import { SnapshotStateBadge } from '~/components/StateBadge' import { makeCrumb } from '~/hooks/use-crumbs' import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' +import { useQuickActions } from '~/hooks/use-quick-actions' import { DiskDetailSideModal } from '~/pages/project/disks/DiskDetailSideModal' import { confirmDelete } from '~/stores/confirm-delete' import { SkeletonCell } from '~/table/cells/EmptyCell' @@ -164,6 +165,19 @@ export default function SnapshotsPage() { columns, emptyState: , }) + + useQuickActions( + useMemo( + () => [ + { + value: 'New snapshot', + onSelect: () => navigate(pb.snapshotsNew({ project })), + }, + ], + [navigate, project] + ) + ) + return ( <> diff --git a/app/pages/project/vpcs/VpcsPage.tsx b/app/pages/project/vpcs/VpcsPage.tsx index 76b7245c3..ab3999cee 100644 --- a/app/pages/project/vpcs/VpcsPage.tsx +++ b/app/pages/project/vpcs/VpcsPage.tsx @@ -29,6 +29,7 @@ import { CreateLink } from '~/ui/lib/CreateButton' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions } from '~/ui/lib/Table' +import { ALL_ISH } from '~/util/consts' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' @@ -128,23 +129,27 @@ export default function VpcsPage() { [project, makeActions] ) - const { table, query } = useQueryTable({ + const { table } = useQueryTable({ query: vpcList(project), columns, emptyState: , }) - const { data: vpcs } = query - + const { data: allVpcs } = useQuery(q(api.vpcList, { query: { project, limit: ALL_ISH } })) useQuickActions( useMemo( - () => - (vpcs?.items || []).map((v) => ({ + () => [ + { + value: 'New VPC', + onSelect: () => navigate(pb.vpcsNew({ project })), + }, + ...(allVpcs?.items || []).map((v) => ({ value: v.name, onSelect: () => navigate(pb.vpc({ project, vpc: v.name })), navGroup: 'Go to VPC', })), - [project, vpcs, navigate] + ], + [project, allVpcs, navigate] ) ) diff --git a/app/pages/system/networking/IpPoolsPage.tsx b/app/pages/system/networking/IpPoolsPage.tsx index 2a16f59c1..48b590230 100644 --- a/app/pages/system/networking/IpPoolsPage.tsx +++ b/app/pages/system/networking/IpPoolsPage.tsx @@ -31,6 +31,7 @@ import { CreateLink } from '~/ui/lib/CreateButton' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions } from '~/ui/lib/Table' +import { ALL_ISH } from '~/util/consts' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' @@ -120,15 +121,15 @@ export default function IpPoolsPage() { ) const columns = useColsWithActions(staticColumns, makeActions) - const { table, query } = useQueryTable({ + const { table } = useQueryTable({ query: ipPoolList, columns, // turn this back on if we expect to see IPv6 ranges regularly // rowHeight: 'large', emptyState: , }) - const { data: pools } = query + const { data: allPools } = useQuery(q(api.ipPoolList, { query: { limit: ALL_ISH } })) useQuickActions( useMemo( () => [ @@ -136,13 +137,13 @@ export default function IpPoolsPage() { value: 'New IP pool', onSelect: () => navigate(pb.ipPoolsNew()), }, - ...(pools?.items || []).map((p) => ({ + ...(allPools?.items || []).map((p) => ({ value: p.name, onSelect: () => navigate(pb.ipPool({ pool: p.name })), navGroup: 'Go to IP pool', })), ], - [navigate, pools] + [navigate, allPools] ) ) diff --git a/app/pages/system/silos/SilosPage.tsx b/app/pages/system/silos/SilosPage.tsx index 9a10ff047..6613ddf87 100644 --- a/app/pages/system/silos/SilosPage.tsx +++ b/app/pages/system/silos/SilosPage.tsx @@ -5,11 +5,12 @@ * * Copyright Oxide Computer Company */ +import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo } from 'react' import { Outlet, useNavigate } from 'react-router' -import { api, getListQFn, queryClient, useApiMutation, type Silo } from '@oxide/api' +import { api, getListQFn, q, queryClient, useApiMutation, type Silo } from '@oxide/api' import { Cloud16Icon, Cloud24Icon } from '@oxide/design-system/icons/react' import { Badge } from '@oxide/design-system/ui' @@ -28,6 +29,7 @@ import { CreateLink } from '~/ui/lib/CreateButton' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions } from '~/ui/lib/Table' +import { ALL_ISH } from '~/util/consts' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' @@ -91,24 +93,24 @@ export default function SilosPage() { ) const columns = useColsWithActions(staticCols, makeActions) - const { table, query } = useQueryTable({ + const { table } = useQueryTable({ query: siloList(), columns, emptyState: , }) - const { data: silos } = query + const { data: allSilos } = useQuery(q(api.siloList, { query: { limit: ALL_ISH } })) useQuickActions( useMemo( () => [ { value: 'New silo', onSelect: () => navigate(pb.silosNew()) }, - ...(silos?.items || []).map((o) => ({ + ...(allSilos?.items || []).map((o) => ({ value: o.name, onSelect: () => navigate(pb.silo({ silo: o.name })), navGroup: 'Silo detail', })), ], - [navigate, silos] + [navigate, allSilos] ) )