diff --git a/.oxlintrc.json b/.oxlintrc.json index fae39f64de..17ac403c4a 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -35,7 +35,13 @@ { // default exports are needed in the route modules and the config files, // but we want to avoid them anywhere else - "files": ["app/pages/**/*", "app/layouts/**/*", "app/forms/**/*", "*.config.ts", "*.config.mjs"], + "files": [ + "app/pages/**/*", + "app/layouts/**/*", + "app/forms/**/*", + "*.config.ts", + "*.config.mjs" + ], "rules": { "import/no-default-export": "off" } diff --git a/app/components/form/fields/SshKeysField.tsx b/app/components/form/fields/SshKeysField.tsx index f177b9a3ac..fab12aea91 100644 --- a/app/components/form/fields/SshKeysField.tsx +++ b/app/components/form/fields/SshKeysField.tsx @@ -12,7 +12,7 @@ import { usePrefetchedApiQuery } from '@oxide/api' import { Key16Icon } from '@oxide/design-system/icons/react' import type { InstanceCreateInput } from '~/forms/instance-create' -import { Component as CreateSSHKeySideModalForm } from '~/forms/ssh-key-create' +import { SSHKeyCreate } from '~/forms/ssh-key-create' import { Button } from '~/ui/lib/Button' import { Checkbox } from '~/ui/lib/Checkbox' import { Divider } from '~/ui/lib/Divider' @@ -138,7 +138,7 @@ export function SshKeysField({ )} {showAddSshKey && ( - setShowAddSshKey(false)} message={ void - /** - * Passing navigate is a bit of a hack to be able to do a nav from the routes - * file. The callers that don't need the arg can ignore it. - */ - onDismiss: (navigate: NavigateFunction) => void + onDismiss: () => void onSuccess?: (disk: Disk) => void unavailableDiskNames?: string[] } @@ -75,14 +70,13 @@ export function CreateDiskSideModalForm({ unavailableDiskNames = [], }: CreateSideModalFormProps) { const queryClient = useApiQueryClient() - const navigate = useNavigate() const createDisk = useApiMutation('diskCreate', { onSuccess(data) { queryClient.invalidateQueries('diskList') addToast(<>Disk {data.name} created) // prettier-ignore onSuccess?.(data) - onDismiss(navigate) + onDismiss() }, }) @@ -123,7 +117,7 @@ export function CreateDiskSideModalForm({ form={form} formType="create" resourceName="disk" - onDismiss={() => onDismiss(navigate)} + onDismiss={onDismiss} onSubmit={({ size, ...rest }) => { const body = { size: size * GiB, ...rest } if (onSubmit) { diff --git a/app/forms/image-edit.tsx b/app/forms/image-edit.tsx index 721460246c..b0d348aee8 100644 --- a/app/forms/image-edit.tsx +++ b/app/forms/image-edit.tsx @@ -6,64 +6,21 @@ * Copyright Oxide Computer Company */ import { useForm } from 'react-hook-form' -import { useNavigate, type LoaderFunctionArgs } from 'react-router' +import { useNavigate } from 'react-router' -import { apiQueryClient, usePrefetchedApiQuery, type Image } from '@oxide/api' +import { type Image } from '@oxide/api' import { Images16Icon } from '@oxide/design-system/icons/react' import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' import { TextField } from '~/components/form/fields/TextField' import { SideModalForm } from '~/components/form/SideModalForm' -import { - getProjectImageSelector, - getSiloImageSelector, - useProjectImageSelector, - useSiloImageSelector, -} from '~/hooks/use-params' import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { ResourceLabel } from '~/ui/lib/SideModal' -import { pb } from '~/util/path-builder' import { capitalize } from '~/util/str' import { bytesToGiB } from '~/util/units' -export const ProjectImageEdit = { - loader: async ({ params }: LoaderFunctionArgs) => { - const { project, image } = getProjectImageSelector(params) - await apiQueryClient.prefetchQuery('imageView', { path: { image }, query: { project } }) - return null - }, - Component: EditProjectImageSideModalForm, -} - -export const SiloImageEdit = { - loader: async ({ params }: LoaderFunctionArgs) => { - const { image } = getSiloImageSelector(params) - await apiQueryClient.prefetchQuery('imageView', { path: { image } }) - return null - }, - Component: EditSiloImageSideModalForm, -} - -function EditProjectImageSideModalForm() { - const { project, image } = useProjectImageSelector() - const { data } = usePrefetchedApiQuery('imageView', { - path: { image }, - query: { project }, - }) - - const dismissLink = pb.projectImages({ project }) - return -} - -function EditSiloImageSideModalForm() { - const { image } = useSiloImageSelector() - const { data } = usePrefetchedApiQuery('imageView', { path: { image } }) - - return -} - -function EditImageSideModalForm({ +export function EditImageSideModalForm({ image, dismissLink, type, diff --git a/app/forms/image-from-snapshot.tsx b/app/forms/image-from-snapshot.tsx index 9be8aa0e59..77a33e1578 100644 --- a/app/forms/image-from-snapshot.tsx +++ b/app/forms/image-from-snapshot.tsx @@ -22,6 +22,7 @@ import { NameField } from '~/components/form/fields/NameField' import { TextField } from '~/components/form/fields/TextField' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { getProjectSnapshotSelector, useProjectSnapshotSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { PropertiesTable } from '~/ui/lib/PropertiesTable' @@ -38,13 +39,15 @@ const defaultValues: Omit = { const snapshotView = ({ project, snapshot }: PP.Snapshot) => apiq('snapshotView', { path: { snapshot }, query: { project } }) -CreateImageFromSnapshotSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, snapshot } = getProjectSnapshotSelector(params) await queryClient.prefetchQuery(snapshotView({ project, snapshot })) return null } -export function CreateImageFromSnapshotSideModalForm() { +export const handle = titleCrumb('Create image from snapshot') + +export default function CreateImageFromSnapshotSideModalForm() { const { snapshot, project } = useProjectSnapshotSelector() const { data } = usePrefetchedQuery(snapshotView({ project, snapshot })) const navigate = useNavigate() diff --git a/app/forms/image-upload.tsx b/app/forms/image-upload.tsx index cec930ca37..6ad5a37e97 100644 --- a/app/forms/image-upload.tsx +++ b/app/forms/image-upload.tsx @@ -35,6 +35,7 @@ import { NameField } from '~/components/form/fields/NameField' import { RadioField } from '~/components/form/fields/RadioField' import { TextField } from '~/components/form/fields/TextField' import { SideModalForm } from '~/components/form/SideModalForm' +import { titleCrumb } from '~/hooks/use-crumbs' import { useProjectSelector } from '~/hooks/use-params' import { Message } from '~/ui/lib/Message' import { Modal } from '~/ui/lib/Modal' @@ -175,11 +176,12 @@ const CHUNK_SIZE_BYTES = 512 * KiB // TODO: make sure cleanup, cancelEverything, and resetMainFlow are called in // the right places -Component.displayName = 'ImageCreate' +export const handle = titleCrumb('Upload image') + /** * Upload an image. Opens a second modal to show upload progress. */ -export function Component() { +export default function ImageCreate() { const navigate = useNavigate() const queryClient = useApiQueryClient() const { project } = useProjectSelector() diff --git a/app/forms/ip-pool-create.tsx b/app/forms/ip-pool-create.tsx index 65fafaeca3..5190d881ce 100644 --- a/app/forms/ip-pool-create.tsx +++ b/app/forms/ip-pool-create.tsx @@ -14,6 +14,7 @@ import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { addToast } from '~/stores/toast' import { Message } from '~/ui/lib/Message' import { pb } from '~/util/path-builder' @@ -23,7 +24,9 @@ const defaultValues: IpPoolCreate = { description: '', } -export function CreateIpPoolSideModalForm() { +export const handle = titleCrumb('New IP pool') + +export default function CreateIpPoolSideModalForm() { const navigate = useNavigate() const queryClient = useApiQueryClient() diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx index 3c92cd8c00..903f038484 100644 --- a/app/forms/ip-pool-edit.tsx +++ b/app/forms/ip-pool-edit.tsx @@ -19,20 +19,22 @@ import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { makeCrumb } from '~/hooks/use-crumbs' import { getIpPoolSelector, useIpPoolSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' import { IpPoolVisibilityMessage } from './ip-pool-create' -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { pool } = getIpPoolSelector(params) await apiQueryClient.prefetchQuery('ipPoolView', { path: { pool } }) return null } -Component.displayName = 'EditIpPoolSideModalForm' -export function Component() { +export const handle = makeCrumb('Edit IP pool') + +export default function EditIpPoolSideModalForm() { const queryClient = useApiQueryClient() const navigate = useNavigate() const poolSelector = useIpPoolSelector() diff --git a/app/forms/ip-pool-range-add.tsx b/app/forms/ip-pool-range-add.tsx index 2039cfaf2d..5697b2e0ea 100644 --- a/app/forms/ip-pool-range-add.tsx +++ b/app/forms/ip-pool-range-add.tsx @@ -12,6 +12,7 @@ import { useApiMutation, useApiQueryClient, type IpRange } from '@oxide/api' import { TextField } from '~/components/form/fields/TextField' import { SideModalForm } from '~/components/form/SideModalForm' +import { titleCrumb } from '~/hooks/use-crumbs' import { useIpPoolSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { Message } from '~/ui/lib/Message' @@ -59,8 +60,9 @@ function resolver(values: IpRange) { return Object.keys(errors).length > 0 ? { values: {}, errors } : { values, errors: {} } } -Component.displayName = 'IpPoolAddRange' -export function Component() { +export const handle = titleCrumb('Add Range') + +export default function IpPoolAddRange() { const { pool } = useIpPoolSelector() const navigate = useNavigate() const queryClient = useApiQueryClient() diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index f7688b3f7d..d43fb08771 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -14,6 +14,7 @@ import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' @@ -22,8 +23,9 @@ const defaultValues: ProjectCreate = { description: '', } -Component.displayName = 'ProjectCreateSideModalForm' -export function Component() { +export const handle = titleCrumb('New project') + +export default function ProjectCreateSideModalForm() { const navigate = useNavigate() const queryClient = useApiQueryClient() diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index a6ffd87b09..11db6968de 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -14,6 +14,7 @@ import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' @@ -21,13 +22,15 @@ import type * as PP from '~/util/path-params' const projectView = ({ project }: PP.Project) => apiq('projectView', { path: { project } }) -EditProjectSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { +export const handle = titleCrumb('Edit project') + +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project } = getProjectSelector(params) await queryClient.prefetchQuery(projectView({ project })) return null } -export function EditProjectSideModalForm() { +export default function EditProjectSideModalForm() { const navigate = useNavigate() const projectSelector = useProjectSelector() diff --git a/app/forms/snapshot-create.tsx b/app/forms/snapshot-create.tsx index 7b15808547..4f98b28fdf 100644 --- a/app/forms/snapshot-create.tsx +++ b/app/forms/snapshot-create.tsx @@ -22,6 +22,7 @@ import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { useProjectSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { toComboboxItems } from '~/ui/lib/Combobox' @@ -42,8 +43,9 @@ const defaultValues: SnapshotCreate = { name: '', } -Component.displayName = 'SnapshotCreate' -export function Component() { +export const handle = titleCrumb('New snapshot') + +export default function SnapshotCreate() { const queryClient = useApiQueryClient() const projectSelector = useProjectSelector() const navigate = useNavigate() diff --git a/app/forms/ssh-key-create.tsx b/app/forms/ssh-key-create.tsx index 0d328b681f..ac4867006e 100644 --- a/app/forms/ssh-key-create.tsx +++ b/app/forms/ssh-key-create.tsx @@ -29,8 +29,7 @@ type Props = { message?: React.ReactNode } -Component.displayName = 'SSHKeyCreate' -export function Component({ onDismiss, message }: Props) { +export function SSHKeyCreate({ onDismiss, message }: Props) { const queryClient = useApiQueryClient() const navigate = useNavigate() diff --git a/app/forms/subnet-create.tsx b/app/forms/subnet-create.tsx index 8728a2572c..62f6731980 100644 --- a/app/forms/subnet-create.tsx +++ b/app/forms/subnet-create.tsx @@ -21,6 +21,7 @@ import { } from '~/components/form/fields/useItemsList' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { useVpcSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { FormDivider } from '~/ui/lib/Divider' @@ -36,7 +37,9 @@ const defaultValues: Required = { customRouter: customRouterDataToForm(undefined), } -export function CreateSubnetForm() { +export const handle = titleCrumb('New Subnet') + +export default function CreateSubnetForm() { const vpcSelector = useVpcSelector() const queryClient = useApiQueryClient() diff --git a/app/forms/subnet-edit.tsx b/app/forms/subnet-edit.tsx index 897c4c2302..661755aa07 100644 --- a/app/forms/subnet-edit.tsx +++ b/app/forms/subnet-edit.tsx @@ -26,6 +26,7 @@ import { } from '~/components/form/fields/useItemsList' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { getVpcSubnetSelector, useVpcSubnetSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { FormDivider } from '~/ui/lib/Divider' @@ -35,13 +36,15 @@ import type * as PP from '~/util/path-params' const subnetView = ({ project, vpc, subnet }: PP.VpcSubnet) => apiq('vpcSubnetView', { query: { project, vpc }, path: { subnet } }) -EditSubnetForm.loader = async ({ params }: LoaderFunctionArgs) => { +export const handle = titleCrumb('Edit Subnet') + +export async function clientLoader({ params }: LoaderFunctionArgs) { const selector = getVpcSubnetSelector(params) await queryClient.prefetchQuery(subnetView(selector)) return null } -export function EditSubnetForm() { +export default function EditSubnetForm() { const subnetSelector = useVpcSubnetSelector() const { project, vpc } = subnetSelector diff --git a/app/forms/vpc-create.tsx b/app/forms/vpc-create.tsx index eda29adf6d..c3d0f23ae0 100644 --- a/app/forms/vpc-create.tsx +++ b/app/forms/vpc-create.tsx @@ -15,6 +15,7 @@ import { NameField } from '~/components/form/fields/NameField' import { TextField } from '~/components/form/fields/TextField' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { useProjectSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' @@ -25,7 +26,9 @@ const defaultValues: VpcCreate = { dnsName: '', } -export function CreateVpcSideModalForm() { +export const handle = titleCrumb('New VPC') + +export default function CreateVpcSideModalForm() { const projectSelector = useProjectSelector() const queryClient = useApiQueryClient() const navigate = useNavigate() diff --git a/app/forms/vpc-router-create.tsx b/app/forms/vpc-router-create.tsx index ca14deb8ff..1132e3051d 100644 --- a/app/forms/vpc-router-create.tsx +++ b/app/forms/vpc-router-create.tsx @@ -14,6 +14,7 @@ import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { useVpcSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' @@ -23,8 +24,9 @@ const defaultValues: VpcRouterCreate = { description: '', } -Component.displayName = 'RouterCreate' -export function Component() { +export const handle = titleCrumb('New Router') + +export default function RouterCreate() { const queryClient = useApiQueryClient() const vpcSelector = useVpcSelector() const navigate = useNavigate() diff --git a/app/forms/vpc-router-edit.tsx b/app/forms/vpc-router-edit.tsx index 3d96605e09..64eb97dc09 100644 --- a/app/forms/vpc-router-edit.tsx +++ b/app/forms/vpc-router-edit.tsx @@ -20,6 +20,7 @@ import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' +import { titleCrumb } from '~/hooks/use-crumbs' import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' @@ -28,13 +29,15 @@ import type * as PP from '~/util/path-params' const routerView = ({ project, vpc, router }: PP.VpcRouter) => apiq('vpcRouterView', { path: { router }, query: { project, vpc } }) -EditRouterSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { +export async function clientLoader({ params }: LoaderFunctionArgs) { const selector = getVpcRouterSelector(params) await queryClient.prefetchQuery(routerView(selector)) return null } -export function EditRouterSideModalForm() { +export const handle = titleCrumb('Edit Router') + +export default function EditRouterSideModalForm() { const routerSelector = useVpcRouterSelector() const { project, vpc, router } = routerSelector const { data: routerData } = usePrefetchedQuery(routerView(routerSelector)) diff --git a/app/forms/vpc-router-route-create.tsx b/app/forms/vpc-router-route-create.tsx index 99c08bb75d..4ea1ecdf80 100644 --- a/app/forms/vpc-router-route-create.tsx +++ b/app/forms/vpc-router-route-create.tsx @@ -13,6 +13,7 @@ import { apiQueryClient, useApiMutation, useApiQueryClient } from '@oxide/api' import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' import { RouteFormFields, type RouteFormValues } from '~/forms/vpc-router-route-common' +import { titleCrumb } from '~/hooks/use-crumbs' import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { ALL_ISH } from '~/util/consts' @@ -25,7 +26,9 @@ const defaultValues: RouteFormValues = { target: { type: 'ip', value: '' }, } -CreateRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { +export const handle = titleCrumb('New Route') + +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, vpc } = getVpcRouterSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('vpcSubnetList', { @@ -41,7 +44,7 @@ CreateRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) = return null } -export function CreateRouterRouteSideModalForm() { +export default function CreateRouterRouteSideModalForm() { const queryClient = useApiQueryClient() const routerSelector = useVpcRouterSelector() const navigate = useNavigate() diff --git a/app/forms/vpc-router-route-edit.tsx b/app/forms/vpc-router-route-edit.tsx index e28211d3f8..742408dcce 100644 --- a/app/forms/vpc-router-route-edit.tsx +++ b/app/forms/vpc-router-route-edit.tsx @@ -23,12 +23,15 @@ import { routeFormMessage, type RouteFormValues, } from '~/forms/vpc-router-route-common' +import { titleCrumb } from '~/hooks/use-crumbs' import { getVpcRouterRouteSelector, useVpcRouterRouteSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { ALL_ISH } from '~/util/consts' import { pb } from '~/util/path-builder' -EditRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { +export const handle = titleCrumb('Edit Route') + +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, vpc, router, route } = getVpcRouterRouteSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('vpcRouterRouteView', { @@ -48,7 +51,7 @@ EditRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => return null } -export function EditRouterRouteSideModalForm() { +export default function EditRouterRouteSideModalForm() { const queryClient = useApiQueryClient() const { route: routeName, ...routerSelector } = useVpcRouterRouteSelector() const navigate = useNavigate() diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index 5e059cf1af..539cf4a0d4 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -13,6 +13,7 @@ import { apiq, getListQFn, queryClient, useApiMutation, type Project } from '@ox import { Folder16Icon, Folder24Icon } from '@oxide/design-system/icons/react' import { DocsPopover } from '~/components/DocsPopover' +import { makeCrumb } from '~/hooks/use-crumbs' import { useQuickActions } from '~/hooks/use-quick-actions' import { confirmDelete } from '~/stores/confirm-delete' import { makeLinkCell } from '~/table/cells/LinkCell' @@ -38,7 +39,7 @@ const EmptyState = () => ( const projectList = getListQFn('projectList', {}) -export async function loader() { +export async function clientLoader() { // fetchQuery instead of prefetchQuery means errors blow up here instead of // waiting to hit the invariant in the useQuery call. We may end up doing this // everywhere, but here in particular it is needed to trigger the silo group @@ -47,6 +48,8 @@ export async function loader() { return null } +export const handle = makeCrumb('Projects', pb.projects()) + const colHelper = createColumnHelper() const staticCols = [ colHelper.accessor('name', { @@ -56,8 +59,7 @@ const staticCols = [ colHelper.accessor('timeCreated', Columns.timeCreated), ] -Component.displayName = 'ProjectsPage' -export function Component() { +export default function ProjectsPage() { const navigate = useNavigate() const { mutateAsync: deleteProject } = useApiMutation('projectDelete', { diff --git a/app/pages/SiloAccessPage.tsx b/app/pages/SiloAccessPage.tsx index 3eabc68f75..1ce31ff2e1 100644 --- a/app/pages/SiloAccessPage.tsx +++ b/app/pages/SiloAccessPage.tsx @@ -52,7 +52,7 @@ const EmptyState = ({ onClick }: { onClick: () => void }) => ( ) -export async function loader() { +export async function clientLoader() { await Promise.all([ apiQueryClient.prefetchQuery('policyView', {}), // used to resolve user names @@ -62,6 +62,8 @@ export async function loader() { return null } +export const handle = { crumb: 'Access' } + type UserRow = { id: string identityType: IdentityType @@ -72,8 +74,7 @@ type UserRow = { const colHelper = createColumnHelper() -Component.displayName = 'SiloAccessPage' -export function Component() { +export default function SiloAccessPage() { const [addModalOpen, setAddModalOpen] = useState(false) const [editingUserRow, setEditingUserRow] = useState(null) diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 0468d0ebfd..b5fbb3e3c5 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -59,7 +59,7 @@ const EmptyState = ({ onClick }: { onClick: () => void }) => ( ) -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project } = getProjectSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('policyView', {}), @@ -71,6 +71,8 @@ export async function loader({ params }: LoaderFunctionArgs) { return null } +export const handle = { crumb: 'Access' } + type UserRow = { id: string identityType: IdentityType @@ -81,8 +83,7 @@ type UserRow = { const colHelper = createColumnHelper() -Component.displayName = 'ProjectAccessPage' -export function Component() { +export default function ProjectAccessPage() { const [addModalOpen, setAddModalOpen] = useState(false) const [editingUserRow, setEditingUserRow] = useState(null) const { project } = useProjectSelector() diff --git a/app/pages/project/disks/DiskCreate.tsx b/app/pages/project/disks/DiskCreate.tsx new file mode 100644 index 0000000000..b3baeb5785 --- /dev/null +++ b/app/pages/project/disks/DiskCreate.tsx @@ -0,0 +1,22 @@ +/* + * 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 { useNavigate } from 'react-router' + +import { CreateDiskSideModalForm } from '~/forms/disk-create' +import { titleCrumb } from '~/hooks/use-crumbs' +import { useProjectSelector } from '~/hooks/use-params' +import { pb } from '~/util/path-builder' + +export const handle = titleCrumb('New disk') + +export default function DiskCreate() { + const navigate = useNavigate() + const { project } = useProjectSelector() + const onDismiss = () => navigate(pb.disks({ project })) + return +} diff --git a/app/pages/project/images/ImagesPage.tsx b/app/pages/project/images/ImagesPage.tsx index fb450c96e9..e58c6bf97c 100644 --- a/app/pages/project/images/ImagesPage.tsx +++ b/app/pages/project/images/ImagesPage.tsx @@ -14,6 +14,7 @@ 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 { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' @@ -45,13 +46,15 @@ const colHelper = createColumnHelper() const imageList = (query: PP.Project) => getListQFn('imageList', { query }) -ImagesPage.loader = async ({ params }: LoaderFunctionArgs) => { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project } = getProjectSelector(params) await queryClient.prefetchQuery(imageList({ project }).optionsFn()) return null } -export function ImagesPage() { +export const handle = makeCrumb('Images', (p) => pb.projectImages(getProjectSelector(p))) + +export default function ImagesPage() { const { project } = useProjectSelector() const [promoteImageName, setPromoteImageName] = useState(null) diff --git a/app/pages/project/images/ProjectImageEdit.tsx b/app/pages/project/images/ProjectImageEdit.tsx new file mode 100644 index 0000000000..5c92a1ae3d --- /dev/null +++ b/app/pages/project/images/ProjectImageEdit.tsx @@ -0,0 +1,34 @@ +/* + * 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 { type LoaderFunctionArgs } from 'react-router' + +import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' + +import { EditImageSideModalForm } from '~/forms/image-edit' +import { titleCrumb } from '~/hooks/use-crumbs' +import { getProjectImageSelector, useProjectImageSelector } from '~/hooks/use-params' +import { pb } from '~/util/path-builder' + +export async function clientLoader({ params }: LoaderFunctionArgs) { + const { project, image } = getProjectImageSelector(params) + await apiQueryClient.prefetchQuery('imageView', { path: { image }, query: { project } }) + return null +} + +export const handle = titleCrumb('Edit Image') + +export default function ProjectImageEdit() { + const { project, image } = useProjectImageSelector() + const { data } = usePrefetchedApiQuery('imageView', { + path: { image }, + query: { project }, + }) + + const dismissLink = pb.projectImages({ project }) + return +} diff --git a/app/pages/project/instances/ConnectTab.tsx b/app/pages/project/instances/ConnectTab.tsx index ca0e518d42..c2cc5a6fa3 100644 --- a/app/pages/project/instances/ConnectTab.tsx +++ b/app/pages/project/instances/ConnectTab.tsx @@ -17,7 +17,7 @@ import { LearnMore, SettingsGroup } from '~/ui/lib/SettingsGroup' import { links } from '~/util/links' import { pb } from '~/util/path-builder' -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, instance } = getInstanceSelector(params) await apiQueryClient.prefetchQuery('instanceExternalIpList', { path: { instance }, @@ -26,8 +26,9 @@ export async function loader({ params }: LoaderFunctionArgs) { return null } -Component.displayName = 'ConnectTab' -export function Component() { +export const handle = { crumb: 'Connect' } + +export default function ConnectTab() { const { project, instance } = useInstanceSelector() const { data: externalIps } = usePrefetchedApiQuery('instanceExternalIpList', { path: { instance }, diff --git a/app/pages/project/instances/InstancePage.tsx b/app/pages/project/instances/InstancePage.tsx index 9b0e150561..6079755b93 100644 --- a/app/pages/project/instances/InstancePage.tsx +++ b/app/pages/project/instances/InstancePage.tsx @@ -73,7 +73,7 @@ async function refreshData() { ]) } -InstancePage.loader = async ({ params }: LoaderFunctionArgs) => { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, instance } = getInstanceSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('instanceView', { @@ -120,7 +120,7 @@ const PollingSpinner = () => ( ) -export function InstancePage() { +export default function InstancePage() { const instanceSelector = useInstanceSelector() const [resizeInstance, setResizeInstance] = useState(false) diff --git a/app/pages/project/instances/NetworkingTab.tsx b/app/pages/project/instances/NetworkingTab.tsx index de416e3242..0a5c685c70 100644 --- a/app/pages/project/instances/NetworkingTab.tsx +++ b/app/pages/project/instances/NetworkingTab.tsx @@ -84,7 +84,7 @@ const SubnetNameFromId = ({ value }: { value: string }) => { return {subnet.name} } -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, instance } = getInstanceSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('instanceNetworkInterfaceList', { @@ -178,8 +178,9 @@ const staticIpCols = [ }), ] -Component.displayName = 'NetworkingTab' -export function Component() { +export const handle = { crumb: 'Networking' } + +export default function NetworkingTab() { const instanceSelector = useInstanceSelector() const { instance: instanceName, project } = instanceSelector diff --git a/app/pages/project/instances/SettingsTab.tsx b/app/pages/project/instances/SettingsTab.tsx index c75492fc86..9a20cca1bd 100644 --- a/app/pages/project/instances/SettingsTab.tsx +++ b/app/pages/project/instances/SettingsTab.tsx @@ -39,8 +39,9 @@ type FormValues = { autoRestartPolicy: FormPolicy } -Component.displayName = 'SettingsTab' -export function Component() { +export const handle = { crumb: 'Settings' } + +export default function SettingsTab() { const instanceSelector = useInstanceSelector() const { data: instance } = usePrefetchedApiQuery('instanceView', { diff --git a/app/pages/project/instances/StorageTab.tsx b/app/pages/project/instances/StorageTab.tsx index 8b790aa1ee..20d51afeff 100644 --- a/app/pages/project/instances/StorageTab.tsx +++ b/app/pages/project/instances/StorageTab.tsx @@ -41,7 +41,7 @@ import { links } from '~/util/links' import { fancifyStates } from './common' -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, instance } = getInstanceSelector(params) const selector = { path: { instance }, query: { project } } await Promise.all([ @@ -75,8 +75,9 @@ const staticCols = [ colHelper.accessor('timeCreated', Columns.timeCreated), ] -Component.displayName = 'StorageTab' -export function Component() { +export const handle = { crumb: 'Storage' } + +export default function StorageTab() { const [showDiskCreate, setShowDiskCreate] = useState(false) const [showDiskAttach, setShowDiskAttach] = useState(false) diff --git a/app/pages/project/snapshots/SnapshotsPage.tsx b/app/pages/project/snapshots/SnapshotsPage.tsx index f72f21ff33..416152ecc0 100644 --- a/app/pages/project/snapshots/SnapshotsPage.tsx +++ b/app/pages/project/snapshots/SnapshotsPage.tsx @@ -22,6 +22,7 @@ import { Snapshots16Icon, Snapshots24Icon } from '@oxide/design-system/icons/rea import { DocsPopover } from '~/components/DocsPopover' import { SnapshotStateBadge } from '~/components/StateBadge' +import { makeCrumb } from '~/hooks/use-crumbs' import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' import { confirmDelete } from '~/stores/confirm-delete' import { SkeletonCell } from '~/table/cells/EmptyCell' @@ -56,7 +57,7 @@ const EmptyState = () => ( const snapshotList = (project: string) => getListQFn('snapshotList', { query: { project } }) -SnapshotsPage.loader = async ({ params }: LoaderFunctionArgs) => { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project } = getProjectSelector(params) await Promise.all([ queryClient.prefetchQuery(snapshotList(project).optionsFn()), @@ -84,6 +85,8 @@ SnapshotsPage.loader = async ({ params }: LoaderFunctionArgs) => { return null } +export const handle = makeCrumb('Snapshots', (p) => pb.snapshots(getProjectSelector(p))) + const colHelper = createColumnHelper() const staticCols = [ colHelper.accessor('name', {}), @@ -99,7 +102,7 @@ const staticCols = [ colHelper.accessor('timeCreated', Columns.timeCreated), ] -export function SnapshotsPage() { +export default function SnapshotsPage() { const queryClient = useApiQueryClient() const { project } = useProjectSelector() const navigate = useNavigate() diff --git a/app/pages/project/vpcs/RouterPage.tsx b/app/pages/project/vpcs/RouterPage.tsx index e07708f34f..baa40c60d9 100644 --- a/app/pages/project/vpcs/RouterPage.tsx +++ b/app/pages/project/vpcs/RouterPage.tsx @@ -27,6 +27,7 @@ import { DocsPopover } from '~/components/DocsPopover' import { HL } from '~/components/HL' 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 { confirmAction } from '~/stores/confirm-action' import { addToast } from '~/stores/toast' @@ -43,12 +44,14 @@ import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' +export const handle = makeCrumb((p) => p.router!) + const routerView = ({ project, vpc, router }: PP.VpcRouter) => apiq('vpcRouterView', { path: { router }, query: { vpc, project } }) const routeList = (query: PP.VpcRouter) => getListQFn('vpcRouterRouteList', { query }) -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const routerSelector = getVpcRouterSelector(params) await Promise.all([ queryClient.prefetchQuery(routerView(routerSelector)), @@ -82,8 +85,7 @@ const RouterRouteTypeValueBadge = ({ ) } -Component.displayName = 'RouterPage' -export function Component() { +export default function RouterPage() { const { project, vpc, router } = useVpcRouterSelector() const { data: routerData } = usePrefetchedQuery(routerView({ project, vpc, router })) diff --git a/app/pages/project/vpcs/VpcFirewallRulesTab.tsx b/app/pages/project/vpcs/VpcFirewallRulesTab.tsx index 7528344d44..87b21de381 100644 --- a/app/pages/project/vpcs/VpcFirewallRulesTab.tsx +++ b/app/pages/project/vpcs/VpcFirewallRulesTab.tsx @@ -100,13 +100,13 @@ const staticColumns = [ const rulesView = (query: PP.Vpc) => apiq('vpcFirewallRulesView', { query }) -VpcFirewallRulesTab.loader = async ({ params }: LoaderFunctionArgs) => { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, vpc } = getVpcSelector(params) await queryClient.prefetchQuery(rulesView({ project, vpc })) return null } -export function VpcFirewallRulesTab() { +export default function VpcFirewallRulesTab() { const vpcSelector = useVpcSelector() const { data } = usePrefetchedQuery(rulesView(vpcSelector)) diff --git a/app/pages/project/vpcs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcGatewaysTab.tsx index 53c9c16e16..e1baf74d74 100644 --- a/app/pages/project/vpcs/VpcGatewaysTab.tsx +++ b/app/pages/project/vpcs/VpcGatewaysTab.tsx @@ -32,6 +32,8 @@ import { useGatewayRoutes, } from './gateway-data' +export const handle = { crumb: 'Internet Gateways' } + const gatewayList = ({ project, vpc }: PP.Vpc) => getListQFn('internetGatewayList', { query: { project, vpc, limit: ALL_ISH } }) const projectIpPoolList = getListQFn('projectIpPoolList', { query: { limit: ALL_ISH } }) @@ -57,7 +59,7 @@ const GatewayRoutes = ({ project, vpc, gateway }: PP.VpcInternetGateway) => { const colHelper = createColumnHelper() -VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, vpc } = getVpcSelector(params) const [gateways, routers] = await Promise.all([ queryClient.fetchQuery(gatewayList({ project, vpc }).optionsFn()), @@ -89,7 +91,7 @@ VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => { return null } -export function VpcInternetGatewaysTab() { +export default function VpcInternetGatewaysTab() { const { project, vpc } = useVpcSelector() const emptyState = ( diff --git a/app/pages/project/vpcs/VpcSubnetsTab.tsx b/app/pages/project/vpcs/VpcSubnetsTab.tsx index b5ea965998..354c198648 100644 --- a/app/pages/project/vpcs/VpcSubnetsTab.tsx +++ b/app/pages/project/vpcs/VpcSubnetsTab.tsx @@ -35,14 +35,15 @@ const colHelper = createColumnHelper() const subnetList = (params: PP.Vpc) => getListQFn('vpcSubnetList', { query: params }) -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, vpc } = getVpcSelector(params) await queryClient.prefetchQuery(subnetList({ project, vpc }).optionsFn()) return null } -Component.displayName = 'VpcSubnetsTab' -export function Component() { +export const handle = { crumb: 'Subnets' } + +export default function VpcSubnetsTab() { const vpcSelector = useVpcSelector() const queryClient = useApiQueryClient() diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index b62448726d..59af8c3e0a 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -14,6 +14,7 @@ import { Gateway16Icon } from '@oxide/design-system/icons/react' import { apiQueryClient, queryClient, usePrefetchedApiQuery } from '~/api' import { SideModalForm } from '~/components/form/SideModalForm' +import { titleCrumb } from '~/hooks/use-crumbs' import { getInternetGatewaySelector, useInternetGatewaySelector } from '~/hooks/use-params' import { IpPoolCell } from '~/table/cells/IpPoolCell' import { CopyableIp } from '~/ui/lib/CopyableIp' @@ -34,6 +35,8 @@ import { useGatewayRoutes, } from './gateway-data' +export const handle = titleCrumb('Edit Internet Gateway') + const RoutesEmpty = () => ( @@ -63,7 +66,7 @@ function RouteRows({ project, vpc, gateway }: PP.VpcInternetGateway) { )) } -EditInternetGatewayForm.loader = async function ({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, vpc, gateway } = getInternetGatewaySelector(params) await Promise.all([ apiQueryClient.prefetchQuery('internetGatewayView', { @@ -82,7 +85,7 @@ EditInternetGatewayForm.loader = async function ({ params }: LoaderFunctionArgs) return null } -export function EditInternetGatewayForm() { +export default function EditInternetGatewayForm() { const navigate = useNavigate() const { project, vpc, gateway } = useInternetGatewaySelector() const onDismiss = () => navigate(pb.vpcInternetGateways({ project, vpc })) diff --git a/app/pages/settings/ssh-key-create.tsx b/app/pages/settings/ssh-key-create.tsx new file mode 100644 index 0000000000..3b934c44c1 --- /dev/null +++ b/app/pages/settings/ssh-key-create.tsx @@ -0,0 +1,15 @@ +/* + * 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 { SSHKeyCreate } from '~/forms/ssh-key-create' +import { titleCrumb } from '~/hooks/use-crumbs' + +export const handle = titleCrumb('New SSH key') + +export default function SSHKeyCreatePage() { + return +} diff --git a/app/pages/system/SiloImageEdit.tsx b/app/pages/system/SiloImageEdit.tsx new file mode 100644 index 0000000000..178a096152 --- /dev/null +++ b/app/pages/system/SiloImageEdit.tsx @@ -0,0 +1,30 @@ +/* + * 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 { type LoaderFunctionArgs } from 'react-router' + +import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' + +import { EditImageSideModalForm } from '~/forms/image-edit' +import { titleCrumb } from '~/hooks/use-crumbs' +import { getSiloImageSelector, useSiloImageSelector } from '~/hooks/use-params' +import { pb } from '~/util/path-builder' + +export async function clientLoader({ params }: LoaderFunctionArgs) { + const { image } = getSiloImageSelector(params) + await apiQueryClient.prefetchQuery('imageView', { path: { image } }) + return null +} + +export const handle = titleCrumb('Edit Image') + +export default function SiloImageEdit() { + const { image } = useSiloImageSelector() + const { data } = usePrefetchedApiQuery('imageView', { path: { image } }) + + return +} diff --git a/app/pages/system/silos/SiloPage.tsx b/app/pages/system/silos/SiloPage.tsx index 2010a068e9..609842d794 100644 --- a/app/pages/system/silos/SiloPage.tsx +++ b/app/pages/system/silos/SiloPage.tsx @@ -12,6 +12,7 @@ import { Cloud16Icon, Cloud24Icon, NextArrow12Icon } from '@oxide/design-system/ import { DocsPopover } from '~/components/DocsPopover' import { QueryParamTabs } from '~/components/QueryParamTabs' +import { makeCrumb } from '~/hooks/use-crumbs' import { getSiloSelector, useSiloSelector } from '~/hooks/use-params' import { Badge } from '~/ui/lib/Badge' import { EmptyMessage } from '~/ui/lib/EmptyMessage' @@ -25,7 +26,7 @@ import { siloIdpList, SiloIdpsTab } from './SiloIdpsTab' import { siloIpPoolsQuery, SiloIpPoolsTab } from './SiloIpPoolsTab' import { SiloQuotasTab } from './SiloQuotasTab' -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { silo } = getSiloSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('siloView', { path: { silo } }), @@ -36,8 +37,9 @@ export async function loader({ params }: LoaderFunctionArgs) { return null } -Component.displayName = 'SiloPage' -export function Component() { +export const handle = makeCrumb((p) => p.silo!) + +export default function SiloPage() { const siloSelector = useSiloSelector() const { data: silo } = usePrefetchedApiQuery('siloView', { path: siloSelector }) diff --git a/app/pages/system/silos/SilosPage.tsx b/app/pages/system/silos/SilosPage.tsx index 62f2dbaa0b..7c15d5763c 100644 --- a/app/pages/system/silos/SilosPage.tsx +++ b/app/pages/system/silos/SilosPage.tsx @@ -20,6 +20,7 @@ import { Cloud16Icon, Cloud24Icon } 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' @@ -64,18 +65,19 @@ const staticCols = [ colHelper.accessor('timeCreated', Columns.timeCreated), ] -export async function loader() { +export async function clientLoader() { await queryClient.prefetchQuery(siloList().optionsFn()) return null } -Component.displayName = 'SilosPage' -export function Component() { +export const handle = makeCrumb('Silos', pb.silos()) + +export default function SilosPage() { const navigate = useNavigate() const queryClient = useApiQueryClient() const { mutateAsync: deleteSilo } = useApiMutation('siloDelete', { - onSuccess(silo, { path }) { + onSuccess(_silo, { path }) { queryClient.invalidateQueries('siloList') addToast(<>Silo {path.silo} deleted) // prettier-ignore }, diff --git a/app/routes.tsx b/app/routes.tsx index 5995702267..9425f9e7e7 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -15,44 +15,11 @@ import { } from 'react-router' import { NotFound } from './components/ErrorPage' -import { CreateDiskSideModalForm } from './forms/disk-create' -import { ProjectImageEdit, SiloImageEdit } from './forms/image-edit' -import { CreateImageFromSnapshotSideModalForm } from './forms/image-from-snapshot' -import * as ImageCreate from './forms/image-upload' -import { CreateIpPoolSideModalForm } from './forms/ip-pool-create' -import * as IpPoolEdit from './forms/ip-pool-edit' -import * as IpPoolAddRange from './forms/ip-pool-range-add' -import * as ProjectCreate from './forms/project-create' -import { EditProjectSideModalForm } from './forms/project-edit' -import * as SnapshotCreate from './forms/snapshot-create' -import * as SSHKeyCreate from './forms/ssh-key-create' -import { CreateSubnetForm } from './forms/subnet-create' -import { EditSubnetForm } from './forms/subnet-edit' -import { CreateVpcSideModalForm } from './forms/vpc-create' -import * as RouterCreate from './forms/vpc-router-create' -import { EditRouterSideModalForm } from './forms/vpc-router-edit' -import { CreateRouterRouteSideModalForm } from './forms/vpc-router-route-create' -import { EditRouterRouteSideModalForm } from './forms/vpc-router-route-edit' -import { makeCrumb, titleCrumb, type Crumb } from './hooks/use-crumbs' +import { makeCrumb, type Crumb } from './hooks/use-crumbs' import { getInstanceSelector, getProjectSelector, getVpcSelector } from './hooks/use-params' -import * as ProjectAccess from './pages/project/access/ProjectAccessPage' -import { ImagesPage } from './pages/project/images/ImagesPage' -import * as ConnectTab from './pages/project/instances/ConnectTab' -import { InstancePage } from './pages/project/instances/InstancePage' -import * as NetworkingTab from './pages/project/instances/NetworkingTab' -import * as SettingsTab from './pages/project/instances/SettingsTab' -import * as StorageTab from './pages/project/instances/StorageTab' -import { SnapshotsPage } from './pages/project/snapshots/SnapshotsPage' import * as VpcRoutersTab from './pages/project/vpcs//VpcRoutersTab' -import { EditInternetGatewayForm } from './pages/project/vpcs/internet-gateway-edit' -import * as RouterPage from './pages/project/vpcs/RouterPage' -import { VpcFirewallRulesTab } from './pages/project/vpcs/VpcFirewallRulesTab' -import { VpcInternetGatewaysTab } from './pages/project/vpcs/VpcGatewaysTab' import { VpcPage } from './pages/project/vpcs/VpcPage' import { VpcsPage } from './pages/project/vpcs/VpcsPage' -import * as VpcSubnetsTab from './pages/project/vpcs/VpcSubnetsTab' -import * as Projects from './pages/ProjectsPage' -import * as SiloAccess from './pages/SiloAccessPage' import * as DisksTab from './pages/system/inventory/DisksTab' import { InventoryPage } from './pages/system/inventory/InventoryPage' import * as SledInstances from './pages/system/inventory/sled/SledInstancesTab' @@ -61,8 +28,6 @@ import * as SledsTab from './pages/system/inventory/SledsTab' import * as IpPool from './pages/system/networking/IpPoolPage' import * as IpPools from './pages/system/networking/IpPoolsPage' import * as SiloImages from './pages/system/SiloImagesPage' -import * as SiloPage from './pages/system/silos/SiloPage' -import * as SilosPage from './pages/system/silos/SilosPage' import { truncate } from './ui/lib/Truncate' import { pb } from './util/path-builder' @@ -127,12 +92,15 @@ export const routes = createRoutesFromElements( lazy={() => import('./forms/ssh-key-edit').then(convert)} /> - + import('./pages/settings/ssh-key-create').then(convert)} + /> import('./layouts/SystemLayout').then(convert)}> - + import('./pages/system/silos/SilosPage').then(convert)}> - p.silo!)}> + import('./pages/system/silos/SiloPage').then(convert)} + > import('./forms/idp/create').then(convert)} @@ -190,13 +161,19 @@ export const routes = createRoutesFromElements( } /> - } /> + import('./forms/ip-pool-create').then(convert)} + /> p.pool!)}> - - + import('./forms/ip-pool-edit').then(convert)} /> + import('./forms/ip-pool-range-add').then(convert)} + /> @@ -205,7 +182,10 @@ export const routes = createRoutesFromElements( import('./layouts/SiloLayout').then(convert)}> - + import('./pages/system/SiloImageEdit').then(convert)} + /> {/* these are here instead of under projects because they need to use SiloLayout */} - + import('./pages/ProjectsPage').then(convert)}> import('./forms/project-create').then(convert)} /> } - loader={EditProjectSideModalForm.loader} - handle={titleCrumb('Edit project')} + lazy={() => import('./forms/project-edit').then(convert)} /> - + import('./pages/SiloAccessPage').then(convert)} /> {/* PROJECT */} @@ -281,12 +258,18 @@ export const routes = createRoutesFromElements( )} > } /> - } loader={InstancePage.loader}> - + import('./pages/project/instances/InstancePage').then(convert)} + > + import('./pages/project/instances/StorageTab').then(convert)} + /> + import('./pages/project/instances/NetworkingTab').then(convert) + } /> - - + import('./pages/project/instances/ConnectTab').then(convert)} + /> + import('./pages/project/instances/SettingsTab').then(convert)} + /> @@ -326,8 +315,7 @@ export const routes = createRoutesFromElements( } - handle={titleCrumb('New VPC')} + lazy={() => import('./forms/vpc-create').then(convert)} /> @@ -341,12 +329,22 @@ export const routes = createRoutesFromElements( } loader={VpcPage.loader}> } - loader={VpcFirewallRulesTab.loader} + // janky one. we only want the loader. we'll have to make this + // its own file eventually. unfortunately the loader can't + // do redirect() with a replace + lazy={() => + import('./pages/project/vpcs/VpcFirewallRulesTab') + .then(convert) + .then(({ loader }) => ({ + loader, + Component: () => , + })) + } /> } - loader={VpcFirewallRulesTab.loader} + lazy={() => + import('./pages/project/vpcs/VpcFirewallRulesTab').then(convert) + } > - + import('./forms/firewall-rules-create').then(convert)} @@ -368,46 +366,40 @@ export const routes = createRoutesFromElements( /> - + import('./pages/project/vpcs/VpcSubnetsTab').then(convert)} + > } - handle={titleCrumb('New Subnet')} + lazy={() => import('./forms/subnet-create').then(convert)} /> } - loader={EditSubnetForm.loader} - handle={titleCrumb('Edit Subnet')} + lazy={() => import('./forms/subnet-edit').then(convert)} /> } - loader={EditRouterSideModalForm.loader} - handle={titleCrumb('Edit Router')} + lazy={() => import('./forms/vpc-router-edit').then(convert)} /> import('./forms/vpc-router-create').then(convert)} /> } + lazy={() => import('./pages/project/vpcs/VpcGatewaysTab').then(convert)} > } - loader={EditInternetGatewayForm.loader} - handle={titleCrumb('Edit Internet Gateway')} + lazy={() => + import('./pages/project/vpcs/internet-gateway-edit').then(convert) + } /> @@ -416,20 +408,19 @@ export const routes = createRoutesFromElements( p.vpc!)}> - p.router!)}> + import('./pages/project/vpcs/RouterPage').then(convert)} + > } - loader={CreateRouterRouteSideModalForm.loader} - handle={titleCrumb('New Route')} + lazy={() => import('./forms/vpc-router-route-create').then(convert)} /> } - loader={EditRouterRouteSideModalForm.loader} - handle={titleCrumb('Edit Route')} + lazy={() => import('./forms/vpc-router-route-edit').then(convert)} /> @@ -455,46 +446,37 @@ export const routes = createRoutesFromElements( navigate('../disks')} /> - } - handle={titleCrumb('New disk')} + lazy={() => import('./pages/project/disks/DiskCreate').then(convert)} /> } - handle={makeCrumb('Snapshots', (p) => pb.snapshots(getProjectSelector(p)))} - loader={SnapshotsPage.loader} + lazy={() => import('./pages/project/snapshots/SnapshotsPage').then(convert)} > import('./forms/snapshot-create').then(convert)} /> } - loader={CreateImageFromSnapshotSideModalForm.loader} - handle={titleCrumb('Create image from snapshot')} + lazy={() => import('./forms/image-from-snapshot').then(convert)} /> - } - handle={makeCrumb('Images', (p) => pb.projectImages(getProjectSelector(p)))} - loader={ImagesPage.loader} - > + import('./pages/project/images/ImagesPage').then(convert)}> - + import('./forms/image-upload').then(convert)} + /> import('./pages/project/images/ProjectImageEdit').then(convert)} /> - + import('./pages/project/access/ProjectAccessPage').then(convert)} + />