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
3 changes: 2 additions & 1 deletion app/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const useCrumbs = () =>
useMatches()
.map((m) => {
invariant(
!m.handle || ['string', 'function'].includes(typeof m.handle.crumb),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
!m.handle || ['string', 'function'].includes(typeof (m.handle as any).crumb),
`Route crumb must be a string or function if present. Check Route for ${m.pathname}.`
)
return m as ValidatedMatch
Expand Down
3 changes: 2 additions & 1 deletion app/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const ErrorBoundary = (props: { children: React.ReactNode }) => (
)

export function RouterDataErrorBoundary() {
const error = useRouteError()
// TODO: validate this unknown at runtime _before_ passing to ErrorFallback
const error = useRouteError() as Props['error']
return <ErrorFallback error={error} />
}
33 changes: 22 additions & 11 deletions app/hooks/use-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ import invariant from 'tiny-invariant'
const err = (param: string) =>
`Param '${param}' not found in route. You might be rendering a component under the wrong route.`

export const requireParams =
<K extends string = never>(...requiredKeys: K[]) =>
(params: Readonly<Params<string>>) => {
const requiredParams: { [k in K]?: string } = {}
if (process.env.NODE_ENV !== 'production') {
for (const k of requiredKeys) {
const value = params[k]
invariant(k in params && value, err(k))
requiredParams[k] = value
}
}
return requiredParams as { readonly [k in K]: string }
}

export const requireOrgParams = requireParams('orgName')
export const requireProjectParams = requireParams('orgName', 'projectName')
export const requireInstanceParams = requireParams('orgName', 'projectName', 'instanceName')
export const requireVpcParams = requireParams('orgName', 'projectName', 'vpcName')

export const useVpcParams = () => requireVpcParams(useParams())

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

could live without the helpers but they're kind of nice?

Copy link
Contributor

@just-be-dev just-be-dev Aug 1, 2022

Choose a reason for hiding this comment

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

Yeah, I think they're good. Shortens the params and makes them a bit explicit, so I'm good with it.

/**
* Wrapper for RR's `useParams` that guarantees (in dev) that the specified
* params are present. No keys besides those specified are present on the result
Expand All @@ -13,17 +34,7 @@ const err = (param: string) =>
// default of never is required to prevent the highly undesirable property that if
// you don't pass any arguments, the result object thinks every property is defined
export function useRequiredParams<K extends string = never>(...requiredKeys: K[]) {
const params = useParams()
// same as below except we build an object with only the specified keys
const requiredParams: { [k in K]?: string } = {}
if (process.env.NODE_ENV !== 'production') {
for (const k of requiredKeys) {
const value = params[k]
invariant(k in params && value, err(k))
requiredParams[k] = value
}
}
return requiredParams as { readonly [k in K]: string }
return requireParams(...requiredKeys)(useParams())
}

/**
Expand Down
13 changes: 2 additions & 11 deletions app/main.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { QueryClientProvider } from '@tanstack/react-query'
import { StrictMode } from 'react'
import ReactDOM from 'react-dom'

import { queryClient } from '@oxide/api'
import { SkipLink } from '@oxide/ui'

import { ErrorBoundary } from './components/ErrorBoundary'
Expand All @@ -18,16 +19,6 @@ if (process.env.SHA) {
)
}

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: 2000,
networkMode: 'offlineFirst',
},
},
})

function render() {
ReactDOM.render(
<StrictMode>
Expand Down
18 changes: 14 additions & 4 deletions app/pages/OrgAccessPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createColumnHelper } from '@tanstack/react-table'
import { getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useMemo, useState } from 'react'
import type { LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
orgRoleOrder,
setUserRole,
useApiMutation,
Expand All @@ -24,9 +26,7 @@ import {
} from '@oxide/ui'

import { OrgAccessAddUserSideModal, OrgAccessEditUserSideModal } from 'app/forms/org-access'
import { useRequiredParams } from 'app/hooks'

type UserRow = UserAccessRow<OrganizationRole>
import { requireOrgParams, useRequiredParams } from 'app/hooks'

const EmptyState = ({ onClick }: { onClick: () => void }) => (
<TableEmptyBox>
Expand All @@ -40,9 +40,19 @@ const EmptyState = ({ onClick }: { onClick: () => void }) => (
</TableEmptyBox>
)

OrgAccessPage.loader = async ({ params }: LoaderFunctionArgs) => {
await Promise.all([
apiQueryClient.prefetchQuery('organizationPolicyView', requireOrgParams(params)),
// used in useUserAccessRows to resolve user names
apiQueryClient.prefetchQuery('userList', { limit: 200 }),
])
}
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 Promise.all is nifty. so easy. I heard you like parallel loading so I put parallel loading in your parallel loading


type UserRow = UserAccessRow<OrganizationRole>

const colHelper = createColumnHelper<UserRow>()

export const OrgAccessPage = () => {
export function OrgAccessPage() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

changed all of these to use function because of hoisting — it lets me define OrgAccessPage.loader above rather than below

const [addModalOpen, setAddModalOpen] = useState(false)
const [editingUserRow, setEditingUserRow] = useState<UserRow | null>(null)
const orgParams = useRequiredParams('orgName')
Expand Down
9 changes: 6 additions & 3 deletions app/pages/OrgsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'

import type { Organization } from '@oxide/api'
import { apiQueryClient } from '@oxide/api'
import { useApiQueryClient } from '@oxide/api'
import { useApiMutation, useApiQuery } from '@oxide/api'
import type { MenuAction } from '@oxide/table'
Expand Down Expand Up @@ -30,11 +31,15 @@ const EmptyState = () => (
/>
)

OrgsPage.loader = async () => {
await apiQueryClient.prefetchQuery('organizationList', { limit: 10 })
}

interface OrgsPageProps {
modal?: 'createOrg' | 'editOrg'
}

const OrgsPage = ({ modal }: OrgsPageProps) => {
export default function OrgsPage({ modal }: OrgsPageProps) {
const navigate = useNavigate()
const location = useLocation()

Expand Down Expand Up @@ -107,5 +112,3 @@ const OrgsPage = ({ modal }: OrgsPageProps) => {
</>
)
}

export default OrgsPage
15 changes: 11 additions & 4 deletions app/pages/ProjectsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useMemo } from 'react'
import type { LoaderFunctionArgs } from 'react-router-dom'
import { Link, useLocation, useNavigate } from 'react-router-dom'

import type { Project } from '@oxide/api'
import { apiQueryClient } from '@oxide/api'
import { useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api'
import type { MenuAction } from '@oxide/table'
import { DateCell, linkCell, useQueryTable } from '@oxide/table'
Expand All @@ -17,7 +19,7 @@ import {
import CreateProjectSideModalForm from 'app/forms/project-create'
import EditProjectSideModalForm from 'app/forms/project-edit'

import { useQuickActions, useRequiredParams } from '../hooks'
import { requireOrgParams, useQuickActions, useRequiredParams } from '../hooks'

const EmptyState = () => (
<EmptyMessage
Expand All @@ -29,11 +31,18 @@ const EmptyState = () => (
/>
)

ProjectsPage.loader = async ({ params }: LoaderFunctionArgs) => {
await apiQueryClient.prefetchQuery('projectList', {
...requireOrgParams(params),
limit: 10,
})
}

interface ProjectsPageProps {
modal?: 'createProject' | 'editProject'
}

const ProjectsPage = ({ modal }: ProjectsPageProps) => {
export default function ProjectsPage({ modal }: ProjectsPageProps) {
const navigate = useNavigate()
const location = useLocation()

Expand Down Expand Up @@ -113,5 +122,3 @@ const ProjectsPage = ({ modal }: ProjectsPageProps) => {
</>
)
}

export default ProjectsPage
14 changes: 12 additions & 2 deletions app/pages/project/access/ProjectAccessPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createColumnHelper } from '@tanstack/react-table'
import { getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useMemo, useState } from 'react'
import type { LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
projectRoleOrder,
setUserRole,
useApiMutation,
Expand All @@ -27,7 +29,7 @@ import {
ProjectAccessAddUserSideModal,
ProjectAccessEditUserSideModal,
} from 'app/forms/project-access'
import { useRequiredParams } from 'app/hooks'
import { requireProjectParams, useRequiredParams } from 'app/hooks'

const EmptyState = ({ onClick }: { onClick: () => void }) => (
<TableEmptyBox>
Expand All @@ -41,11 +43,19 @@ const EmptyState = ({ onClick }: { onClick: () => void }) => (
</TableEmptyBox>
)

ProjectAccessPage.loader = async ({ params }: LoaderFunctionArgs) => {
await Promise.all([
apiQueryClient.prefetchQuery('projectPolicyView', requireProjectParams(params)),
// used in useUserAccessRows to resolve user names
apiQueryClient.prefetchQuery('userList', { limit: 200 }),
])
}

type UserRow = UserAccessRow<ProjectRole>

const colHelper = createColumnHelper<UserRow>()

export const ProjectAccessPage = () => {
export function ProjectAccessPage() {
const [addModalOpen, setAddModalOpen] = useState(false)
const [editingUserRow, setEditingUserRow] = useState<UserRow | null>(null)
const projectParams = useRequiredParams('orgName', 'projectName')
Expand Down
11 changes: 10 additions & 1 deletion app/pages/project/disks/DisksPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { LoaderFunctionArgs } from 'react-router-dom'
import { Link, useNavigate } from 'react-router-dom'

import type { Disk } from '@oxide/api'
import { apiQueryClient } from '@oxide/api'
import { useApiMutation, useApiQueryClient } from '@oxide/api'
import { useApiQuery } from '@oxide/api'
import type { MenuAction } from '@oxide/table'
Expand All @@ -18,7 +20,7 @@ import {

import { DiskStatusBadge } from 'app/components/StatusBadge'
import CreateDiskSideModalForm from 'app/forms/disk-create'
import { useRequiredParams } from 'app/hooks'
import { requireProjectParams, useRequiredParams } from 'app/hooks'

function AttachedInstance(props: {
orgName: string
Expand Down Expand Up @@ -50,6 +52,13 @@ interface DisksPageProps {
modal?: 'createDisk'
}

DisksPage.loader = async ({ params }: LoaderFunctionArgs) => {
await apiQueryClient.prefetchQuery('diskList', {
...requireProjectParams(params),
limit: 10,
})
}

export function DisksPage({ modal }: DisksPageProps) {
const navigate = useNavigate()

Expand Down
14 changes: 12 additions & 2 deletions app/pages/project/images/ImagesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { LoaderFunctionArgs } from 'react-router-dom'

import { apiQueryClient } from '@oxide/api'
import { DateCell, SizeCell, useQueryTable } from '@oxide/table'
import { EmptyMessage, Images24Icon, PageHeader, PageTitle } from '@oxide/ui'

import { useRequiredParams } from 'app/hooks'
import { requireProjectParams, useRequiredParams } from 'app/hooks'

const EmptyState = () => (
<EmptyMessage
Expand All @@ -13,7 +16,14 @@ const EmptyState = () => (
/>
)

export const ImagesPage = () => {
ImagesPage.loader = async ({ params }: LoaderFunctionArgs) => {
await apiQueryClient.prefetchQuery('imageList', {
...requireProjectParams(params),
limit: 10,
})
}

export function ImagesPage() {
const projectParams = useRequiredParams('orgName', 'projectName')
const { Table, Column } = useQueryTable('imageList', projectParams)
return (
Expand Down
14 changes: 11 additions & 3 deletions app/pages/project/instances/InstancesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useMemo } from 'react'
import type { LoaderFunctionArgs } from 'react-router-dom'
import { Link, useNavigate } from 'react-router-dom'

import { useApiQuery, useApiQueryClient } from '@oxide/api'
import { apiQueryClient, useApiQuery, useApiQueryClient } from '@oxide/api'
import {
DateCell,
InstanceResourceCell,
Expand All @@ -18,7 +19,7 @@ import {
buttonStyle,
} from '@oxide/ui'

import { useQuickActions, useRequiredParams } from 'app/hooks'
import { requireProjectParams, useQuickActions, useRequiredParams } from 'app/hooks'

import { useMakeInstanceActions } from './actions'

Expand All @@ -32,7 +33,14 @@ const EmptyState = () => (
/>
)

export const InstancesPage = () => {
InstancesPage.loader = async ({ params }: LoaderFunctionArgs) => {
await apiQueryClient.prefetchQuery('instanceList', {
...requireProjectParams(params),
limit: 10,
})
}

export function InstancesPage() {
const projectParams = useRequiredParams('orgName', 'projectName')
const { orgName, projectName } = projectParams

Expand Down
11 changes: 8 additions & 3 deletions app/pages/project/instances/instance/InstancePage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import filesize from 'filesize'
import { memo, useMemo } from 'react'
import type { LoaderFunctionArgs } from 'react-router-dom'
import { useNavigate } from 'react-router-dom'

import { useApiQuery, useApiQueryClient } from '@oxide/api'
import { apiQueryClient, useApiQuery, useApiQueryClient } from '@oxide/api'
import { Instances24Icon, PageHeader, PageTitle, PropertiesTable, Tab } from '@oxide/ui'
import { pick } from '@oxide/util'

import { MoreActionsMenu } from 'app/components/MoreActionsMenu'
import { InstanceStatusBadge } from 'app/components/StatusBadge'
import { Tabs } from 'app/components/Tabs'
import { useQuickActions, useRequiredParams } from 'app/hooks'
import { requireInstanceParams, useQuickActions, useRequiredParams } from 'app/hooks'

import { useMakeInstanceActions } from '../actions'
import { MetricsTab } from './tabs/MetricsTab'
Expand Down Expand Up @@ -38,7 +39,11 @@ const InstanceTabs = memo(() => (
</Tabs>
))

export const InstancePage = () => {
InstancePage.loader = async ({ params }: LoaderFunctionArgs) => {
await apiQueryClient.prefetchQuery('instanceView', requireInstanceParams(params))
}

export function InstancePage() {
const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName')

const navigate = useNavigate()
Expand Down
Loading