From d4e9b4ab63d087706d120f75268b0e9c97c6e995 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 17 Feb 2023 15:07:11 -0600 Subject: [PATCH 01/39] organization list v1 --- app/components/TopBarPicker.tsx | 2 +- app/forms/org-create.tsx | 2 +- app/forms/org-edit.tsx | 2 +- app/pages/OrgsPage.tsx | 8 ++++---- app/pages/SiloUtilizationPage.tsx | 4 ++-- libs/api-mocks/msw/handlers.ts | 7 +++++-- libs/api/__tests__/hooks.spec.tsx | 8 ++++---- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/components/TopBarPicker.tsx b/app/components/TopBarPicker.tsx index 1aa2fee3b..d093fdeb8 100644 --- a/app/components/TopBarPicker.tsx +++ b/app/components/TopBarPicker.tsx @@ -174,7 +174,7 @@ export function SiloPicker() { export function OrgPicker() { const { orgName } = useParams() - const { data } = useApiQuery('organizationList', { query: { limit: 20 } }) + const { data } = useApiQuery('organizationListV1', { query: { limit: 20 } }) const items = (data?.items || []).map(({ name }) => ({ label: name, to: pb.projects({ orgName: name }), diff --git a/app/forms/org-create.tsx b/app/forms/org-create.tsx index 550b776d1..32af5ea13 100644 --- a/app/forms/org-create.tsx +++ b/app/forms/org-create.tsx @@ -20,7 +20,7 @@ export function CreateOrgSideModalForm() { const createOrg = useApiMutation('organizationCreate', { onSuccess(org) { - queryClient.invalidateQueries('organizationList', {}) + queryClient.invalidateQueries('organizationListV1', {}) // avoid the org fetch when the org page loads since we have the data const orgParams = { orgName: org.name } queryClient.setQueryData('organizationView', { path: orgParams }, org) diff --git a/app/forms/org-edit.tsx b/app/forms/org-edit.tsx index 646af35d2..1404b1729 100644 --- a/app/forms/org-edit.tsx +++ b/app/forms/org-edit.tsx @@ -28,7 +28,7 @@ export function EditOrgSideModalForm() { const updateOrg = useApiMutation('organizationUpdate', { onSuccess(org) { - queryClient.invalidateQueries('organizationList', {}) + queryClient.invalidateQueries('organizationListV1', {}) // avoid the org fetch when the org page loads since we have the data queryClient.setQueryData('organizationView', { path: { orgName: org.name } }, org) addToast({ diff --git a/app/pages/OrgsPage.tsx b/app/pages/OrgsPage.tsx index f78a48635..666402f5a 100644 --- a/app/pages/OrgsPage.tsx +++ b/app/pages/OrgsPage.tsx @@ -31,23 +31,23 @@ const EmptyState = () => ( ) OrgsPage.loader = async () => { - await apiQueryClient.prefetchQuery('organizationList', { query: { limit: 10 } }) + await apiQueryClient.prefetchQuery('organizationListV1', { query: { limit: 10 } }) return null } export default function OrgsPage() { const navigate = useNavigate() - const { Table, Column } = useQueryTable('organizationList', {}) + const { Table, Column } = useQueryTable('organizationListV1', {}) const queryClient = useApiQueryClient() - const { data: orgs } = useApiQuery('organizationList', { + const { data: orgs } = useApiQuery('organizationListV1', { query: { limit: 10 }, // to have same params as QueryTable }) const deleteOrg = useApiMutation('organizationDelete', { onSuccess() { - queryClient.invalidateQueries('organizationList', {}) + queryClient.invalidateQueries('organizationListV1', {}) }, }) diff --git a/app/pages/SiloUtilizationPage.tsx b/app/pages/SiloUtilizationPage.tsx index c804791f4..d89b14493 100644 --- a/app/pages/SiloUtilizationPage.tsx +++ b/app/pages/SiloUtilizationPage.tsx @@ -14,7 +14,7 @@ const ALL_PROJECTS = '|ALL_PROJECTS|' const toListboxItem = (x: { name: string; id: string }) => ({ label: x.name, value: x.id }) SiloUtilizationPage.loader = async () => { - await apiQueryClient.prefetchQuery('organizationList', {}) + await apiQueryClient.prefetchQuery('organizationListV1', {}) return null } @@ -24,7 +24,7 @@ export function SiloUtilizationPage() { const [orgId, setOrgId] = useState(siloId) const [projectId, setProjectId] = useState(null) - const { data: orgs } = useApiQuery('organizationList', {}) + const { data: orgs } = useApiQuery('organizationListV1', {}) const orgName = orgs?.items.find((o) => orgId && o.id === orgId)?.name diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index c14aafc8b..d0832a129 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -53,7 +53,7 @@ export const handlers = makeHandlers({ deviceAccessToken: () => 200, groupList: (params) => paginated(params.query, db.userGroups), - organizationList: (params) => paginated(params.query, db.orgs), + organizationListV1: (params) => paginated(params.query, db.orgs), organizationCreate({ body }) { errIfExists(db.orgs, { name: body.name }) @@ -1088,7 +1088,6 @@ export const handlers = makeHandlers({ instanceViewV1: NotImplemented, organizationCreateV1: NotImplemented, organizationDeleteV1: NotImplemented, - organizationListV1: NotImplemented, organizationPolicyUpdateV1: NotImplemented, organizationPolicyViewV1: NotImplemented, organizationUpdateV1: NotImplemented, @@ -1136,4 +1135,8 @@ export const handlers = makeHandlers({ vpcSubnetViewV1: NotImplemented, vpcUpdateV1: NotImplemented, vpcViewV1: NotImplemented, + + // Deprecated endpoints + + organizationList: NotImplemented, }) diff --git a/libs/api/__tests__/hooks.spec.tsx b/libs/api/__tests__/hooks.spec.tsx index ae8c4e7ad..984ae4963 100644 --- a/libs/api/__tests__/hooks.spec.tsx +++ b/libs/api/__tests__/hooks.spec.tsx @@ -26,7 +26,7 @@ export function Wrapper({ children }: { children: React.ReactNode }) { const config = { wrapper: Wrapper } -const renderGetOrgs = () => renderHook(() => useApiQuery('organizationList', {}), config) +const renderGetOrgs = () => renderHook(() => useApiQuery('organizationListV1', {}), config) // 503 is a special key in the MSW server that returns a 503 const renderGetOrg503 = () => @@ -71,7 +71,7 @@ describe('useApiQuery', () => { }) it('contains client_error if error body is not json', async () => { - overrideOnce('get', '/api/organizations', 503, 'not json') + overrideOnce('get', '/api/v1/organizations', 503, 'not json') const { result } = renderGetOrgs() @@ -89,7 +89,7 @@ describe('useApiQuery', () => { }) it('does not client_error if response body is empty', async () => { - overrideOnce('get', '/api/organizations', 503, '') + overrideOnce('get', '/api/v1/organizations', 503, '') const { result } = renderGetOrgs() @@ -156,7 +156,7 @@ describe('useApiQuery', () => { // RQ doesn't like a value of undefined for data, so we're using {} for now it('returns success with empty object if response body is empty', async () => { - overrideOnce('get', '/api/organizations', 204, '') + overrideOnce('get', '/api/v1/organizations', 204, '') const { result } = renderGetOrgs() From a8613269473f064c60e1806bf97008f683645cae Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 17 Feb 2023 15:24:25 -0600 Subject: [PATCH 02/39] project list v1 --- app/components/TopBarPicker.tsx | 4 +++- app/forms/project-create.tsx | 2 +- app/forms/project-edit.tsx | 3 ++- app/pages/ProjectsPage.tsx | 22 +++++++++++++--------- app/pages/SiloUtilizationPage.tsx | 4 ++-- libs/api-mocks/msw/db.ts | 12 +++++++++++- libs/api-mocks/msw/handlers.ts | 13 +++++++++---- libs/api/index.ts | 1 + libs/api/path-params-v1.ts | 7 +++++++ 9 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 libs/api/path-params-v1.ts diff --git a/app/components/TopBarPicker.tsx b/app/components/TopBarPicker.tsx index d093fdeb8..9c6c1b29f 100644 --- a/app/components/TopBarPicker.tsx +++ b/app/components/TopBarPicker.tsx @@ -195,7 +195,9 @@ export function OrgPicker() { export function ProjectPicker() { // picker only shows up when a project is in scope const { orgName, projectName } = useProjectParams() - const { data } = useApiQuery('projectList', { path: { orgName }, query: { limit: 20 } }) + const { data } = useApiQuery('projectListV1', { + query: { organization: orgName, limit: 20 }, + }) const items = (data?.items || []).map(({ name }) => ({ label: name, to: pb.instances({ orgName, projectName: name }), diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index 610365e86..a93a11bc6 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -26,7 +26,7 @@ export function CreateProjectSideModalForm() { const createProject = useApiMutation('projectCreate', { onSuccess(project) { // refetch list of projects in sidebar - queryClient.invalidateQueries('projectList', { path: { orgName } }) + queryClient.invalidateQueries('projectListV1', { query: { organization: orgName } }) // avoid the project fetch when the project page loads since we have the data const projectParams = { orgName, projectName: project.name } queryClient.setQueryData('projectView', { path: projectParams }, project) diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index c8d9a4944..62b5ee561 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -30,7 +30,8 @@ export function EditProjectSideModalForm() { const editProject = useApiMutation('projectUpdate', { onSuccess(project) { // refetch list of projects in sidebar - queryClient.invalidateQueries('projectList', { path: { orgName } }) + // TODO: check this invalidation + queryClient.invalidateQueries('projectListV1', { query: { organization: orgName } }) // avoid the project fetch when the project page loads since we have the data queryClient.setQueryData( 'projectView', diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index fbcc46024..5ba5726d5 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -32,9 +32,9 @@ const EmptyState = () => ( ) ProjectsPage.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('projectList', { - path: requireOrgParams(params), - query: { limit: 10 }, + const { orgName } = requireOrgParams(params) + await apiQueryClient.prefetchQuery('projectListV1', { + query: { organization: orgName, limit: 10 }, }) return null } @@ -44,18 +44,22 @@ export default function ProjectsPage() { const queryClient = useApiQueryClient() const { orgName } = useOrgParams() - const { Table, Column } = useQueryTable('projectList', { - path: { orgName }, + const { Table, Column } = useQueryTable('projectListV1', { + query: { organization: orgName }, }) - const { data: projects } = useApiQuery('projectList', { - path: { orgName }, - query: { limit: 10 }, // to have same params as QueryTable + const { data: projects } = useApiQuery('projectListV1', { + query: { + organization: orgName, + limit: 10, // to have same params as QueryTable + }, }) const deleteProject = useApiMutation('projectDelete', { onSuccess() { - queryClient.invalidateQueries('projectList', { path: { orgName } }) + // TODO: figure out if this is invalidating as expected, can we leave out the query + // altogether, etc. Look at whether limit param matters. + queryClient.invalidateQueries('projectListV1', { query: { organization: orgName } }) }, }) diff --git a/app/pages/SiloUtilizationPage.tsx b/app/pages/SiloUtilizationPage.tsx index d89b14493..7e07fdda7 100644 --- a/app/pages/SiloUtilizationPage.tsx +++ b/app/pages/SiloUtilizationPage.tsx @@ -29,8 +29,8 @@ export function SiloUtilizationPage() { const orgName = orgs?.items.find((o) => orgId && o.id === orgId)?.name const { data: projects } = useApiQuery( - 'projectList', - { path: { orgName: orgName! } }, // only enabled if it's there + 'projectListV1', + { query: { organization: orgName! } }, // only enabled if it's there { enabled: !!orgName } ) diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index e18b49956..6552a06b1 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -1,5 +1,7 @@ +import { validate as isUuid } from 'uuid' + import * as mock from '@oxide/api-mocks' -import type { ApiTypes as Api, PathParams as PP } from '@oxide/api' +import type { ApiTypes as Api, PathParams as PP, PathParamsV1 as PPv1 } from '@oxide/api' import { user1 } from '@oxide/api-mocks' import type { Json } from '../json-type' @@ -23,6 +25,14 @@ export function lookupOrg(params: PP.Org): Json { return org } +export function lookupOrgV1({ organization }: PPv1.Org): Json { + const org = isUuid(organization) + ? db.orgs.find((o) => o.id === organization) + : db.orgs.find((o) => o.name === organization) + if (!org) throw notFoundErr + return org +} + export function lookupProject(params: PP.Project): Json { const org = lookupOrg(params) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index d0832a129..1b6b71321 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -20,6 +20,7 @@ import { lookupInstance, lookupNetworkInterface, lookupOrg, + lookupOrgV1, lookupProject, lookupSamlIdp, lookupSilo, @@ -31,6 +32,7 @@ import { lookupVpcRouter, lookupVpcRouterRoute, lookupVpcSubnet, + notFoundErr, } from './db' import { NotImplemented, @@ -114,10 +116,13 @@ export const handlers = makeHandlers({ return body }, - projectList(params) { - const org = lookupOrg(params.path) - const projects = db.projects.filter((p) => p.organization_id === org.id) + projectListV1(params) { + // TODO: helper like requireOrgParams to do the check and throw if not + const { organization } = params.query + if (!organization) throw notFoundErr + const org = lookupOrgV1({ organization }) + const projects = db.projects.filter((p) => p.organization_id === org.id) return paginated(params.query, projects) }, projectCreate({ body, ...params }) { @@ -1097,7 +1102,6 @@ export const handlers = makeHandlers({ policyViewV1: NotImplemented, projectCreateV1: NotImplemented, projectDeleteV1: NotImplemented, - projectListV1: NotImplemented, projectPolicyUpdateV1: NotImplemented, projectPolicyViewV1: NotImplemented, projectUpdateV1: NotImplemented, @@ -1139,4 +1143,5 @@ export const handlers = makeHandlers({ // Deprecated endpoints organizationList: NotImplemented, + projectList: NotImplemented, }) diff --git a/libs/api/index.ts b/libs/api/index.ts index 532950601..1c8295c8a 100644 --- a/libs/api/index.ts +++ b/libs/api/index.ts @@ -46,6 +46,7 @@ export * as ZVal from './__generated__/validate' export type { ApiTypes } export * as PathParams from './path-params' +export * as PathParamsV1 from './path-params-v1' export type { Params, Result, ResultItem } from './hooks' export { navToLogin } from './nav-to-login' diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts new file mode 100644 index 000000000..f84746af5 --- /dev/null +++ b/libs/api/path-params-v1.ts @@ -0,0 +1,7 @@ +// these aren't really only path params anymore so we'll probably want to rename +// this file +import type { Merge } from 'type-fest' + +export type Org = { organization: string } +export type Project = Merge +export type Instance = Merge From 4cde77b5ffe8cd6daf4bb300ce0731f5e1aaec49 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 09:33:55 -0600 Subject: [PATCH 03/39] org view v1 --- app/forms/org-create.tsx | 6 +++++- app/forms/org-edit.tsx | 15 +++++++++++---- app/pages/OrgsPage.tsx | 7 +++++-- libs/api-mocks/msw/handlers.ts | 8 ++++---- libs/api/__tests__/hooks.spec.tsx | 10 +++++----- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app/forms/org-create.tsx b/app/forms/org-create.tsx index 32af5ea13..7859bf82f 100644 --- a/app/forms/org-create.tsx +++ b/app/forms/org-create.tsx @@ -23,7 +23,11 @@ export function CreateOrgSideModalForm() { queryClient.invalidateQueries('organizationListV1', {}) // avoid the org fetch when the org page loads since we have the data const orgParams = { orgName: org.name } - queryClient.setQueryData('organizationView', { path: orgParams }, org) + queryClient.setQueryData( + 'organizationViewV1', + { path: { organization: org.name } }, + org + ) addToast({ icon: , title: 'Success!', diff --git a/app/forms/org-edit.tsx b/app/forms/org-edit.tsx index 1404b1729..0ec70b4a9 100644 --- a/app/forms/org-edit.tsx +++ b/app/forms/org-edit.tsx @@ -9,8 +9,9 @@ import { requireOrgParams, useOrgParams, useToast } from 'app/hooks' import { pb } from 'app/util/path-builder' EditOrgSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('organizationView', { - path: requireOrgParams(params), + const { orgName } = requireOrgParams(params) + await apiQueryClient.prefetchQuery('organizationViewV1', { + path: { organization: orgName }, }) return null } @@ -24,13 +25,19 @@ export function EditOrgSideModalForm() { const onDismiss = () => navigate(pb.orgs()) - const { data: org } = useApiQuery('organizationView', { path: { orgName } }) + const { data: org } = useApiQuery('organizationViewV1', { + path: { organization: orgName }, + }) const updateOrg = useApiMutation('organizationUpdate', { onSuccess(org) { queryClient.invalidateQueries('organizationListV1', {}) // avoid the org fetch when the org page loads since we have the data - queryClient.setQueryData('organizationView', { path: { orgName: org.name } }, org) + queryClient.setQueryData( + 'organizationViewV1', + { path: { organization: org.name } }, + org + ) addToast({ icon: , title: 'Success!', diff --git a/app/pages/OrgsPage.tsx b/app/pages/OrgsPage.tsx index 666402f5a..3c0e3751b 100644 --- a/app/pages/OrgsPage.tsx +++ b/app/pages/OrgsPage.tsx @@ -55,8 +55,11 @@ export default function OrgsPage() { { label: 'Edit', onActivate() { - const path = { orgName: org.name } - apiQueryClient.setQueryData('organizationView', { path }, org) + apiQueryClient.setQueryData( + 'organizationViewV1', + { path: { organization: org.name } }, + org + ) navigate(pb.orgEdit({ orgName: org.name })) }, }, diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 1b6b71321..841c442f9 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -68,12 +68,12 @@ export const handlers = makeHandlers({ return json(newOrg, { status: 201 }) }, - organizationView(params) { - if (params.path.orgName.endsWith('-error-503')) { + organizationViewV1(params) { + if (params.path.organization.endsWith('-error-503')) { throw unavailableErr } - return lookupOrg(params.path) + return lookupOrgV1(params.path) }, organizationUpdate({ body, ...params }) { const org = lookupOrg(params.path) @@ -1096,7 +1096,6 @@ export const handlers = makeHandlers({ organizationPolicyUpdateV1: NotImplemented, organizationPolicyViewV1: NotImplemented, organizationUpdateV1: NotImplemented, - organizationViewV1: NotImplemented, physicalDiskListV1: NotImplemented, policyUpdateV1: NotImplemented, policyViewV1: NotImplemented, @@ -1144,4 +1143,5 @@ export const handlers = makeHandlers({ organizationList: NotImplemented, projectList: NotImplemented, + organizationView: NotImplemented, }) diff --git a/libs/api/__tests__/hooks.spec.tsx b/libs/api/__tests__/hooks.spec.tsx index 984ae4963..6955fff68 100644 --- a/libs/api/__tests__/hooks.spec.tsx +++ b/libs/api/__tests__/hooks.spec.tsx @@ -31,7 +31,7 @@ const renderGetOrgs = () => renderHook(() => useApiQuery('organizationListV1', { // 503 is a special key in the MSW server that returns a 503 const renderGetOrg503 = () => renderHook( - () => useApiQuery('organizationView', { path: { orgName: 'org-error-503' } }), + () => useApiQuery('organizationViewV1', { path: { organization: 'org-error-503' } }), config ) @@ -110,8 +110,8 @@ describe('useApiQuery', () => { it('throws by default', async () => { const { result } = renderHook( () => - useApiQuery('organizationView', { - path: { orgName: 'nonexistent' }, + useApiQuery('organizationViewV1', { + path: { organization: 'nonexistent' }, }), config ) @@ -129,8 +129,8 @@ describe('useApiQuery', () => { const { result } = renderHook( () => useApiQuery( - 'organizationView', - { path: { orgName: 'nonexistent' } }, + 'organizationViewV1', + { path: { organization: 'nonexistent' } }, { useErrorBoundary: false } // <----- the point ), config From accdd0534900855bbf44e58bb5b7fde222258395 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 10:05:52 -0600 Subject: [PATCH 04/39] project view v1, sorely in need of some helpers --- app/forms/project-create.tsx | 9 +++++++- app/forms/project-edit.tsx | 18 +++++++++++----- app/pages/ProjectsPage.tsx | 9 +++++++- libs/api-mocks/msw/db.ts | 38 +++++++++++++++++++++++++++------- libs/api-mocks/msw/handlers.ts | 12 +++++------ libs/api/path-params-v1.ts | 4 ++-- 6 files changed, 67 insertions(+), 23 deletions(-) diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index a93a11bc6..8605a391f 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -29,7 +29,14 @@ export function CreateProjectSideModalForm() { queryClient.invalidateQueries('projectListV1', { query: { organization: orgName } }) // avoid the project fetch when the project page loads since we have the data const projectParams = { orgName, projectName: project.name } - queryClient.setQueryData('projectView', { path: projectParams }, project) + queryClient.setQueryData( + 'projectViewV1', + { + path: { project: project.name }, + query: { organization: orgName }, + }, + project + ) addToast({ icon: , title: 'Success!', diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index 62b5ee561..d1b5dd2e0 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -10,8 +10,10 @@ import { pb } from 'app/util/path-builder' import { requireProjectParams, useRequiredParams, useToast } from '../hooks' EditProjectSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('projectView', { - path: requireProjectParams(params), + const { projectName, orgName } = requireProjectParams(params) + await apiQueryClient.prefetchQuery('projectViewV1', { + path: { project: projectName }, + query: { organization: orgName }, }) return null } @@ -25,7 +27,10 @@ export function EditProjectSideModalForm() { const onDismiss = () => navigate(pb.projects({ orgName })) - const { data: project } = useApiQuery('projectView', { path: { orgName, projectName } }) + const { data: project } = useApiQuery('projectViewV1', { + path: { project: projectName }, + query: { organization: orgName }, + }) const editProject = useApiMutation('projectUpdate', { onSuccess(project) { @@ -34,8 +39,11 @@ export function EditProjectSideModalForm() { queryClient.invalidateQueries('projectListV1', { query: { organization: orgName } }) // avoid the project fetch when the project page loads since we have the data queryClient.setQueryData( - 'projectView', - { path: { orgName, projectName: project.name } }, + 'projectViewV1', + { + path: { project: project.name }, + query: { organization: orgName }, + }, project ) addToast({ diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index 5ba5726d5..e8ec7d0cd 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -70,7 +70,14 @@ export default function ProjectsPage() { const path = { orgName, projectName: project.name } // the edit view has its own loader, but we can make the modal open // instantaneously by preloading the fetch result - apiQueryClient.setQueryData('projectView', { path }, project) + apiQueryClient.setQueryData( + 'projectViewV1', + { + path: { project: project.name }, + query: { organization: orgName }, + }, + project + ) navigate(pb.projectEdit(path)) }, }, diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 6552a06b1..864025114 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -19,16 +19,38 @@ export const lookupById = return item } -export function lookupOrg(params: PP.Org): Json { - const org = db.orgs.find((o) => o.name === params.orgName) - if (!org) throw notFoundErr - return org +export const lookup = { + org({ organization }: PPv1.Org): Json { + const org = isUuid(organization) + ? db.orgs.find((o) => o.id === organization) + : db.orgs.find((o) => o.name === organization) + if (!org) throw notFoundErr + return org + }, + project(params: PPv1.Project): Json { + const { project: id, ...orgParams } = params + // if we have a project ID, look it up directly, otherwise call lookup org + // with the other params to get an org ID, then look it up by org ID and name + if (isUuid(id)) { + const project = db.projects.find((p) => p.id === id) + if (!project) throw notFoundErr + return project + } + + const { organization } = orgParams + if (!organization) throw notFoundErr + + const org = lookup.org({ organization }) + + const project = db.projects.find((p) => p.organization_id === org.id && p.name === id) + if (!project) throw notFoundErr + + return project + }, } -export function lookupOrgV1({ organization }: PPv1.Org): Json { - const org = isUuid(organization) - ? db.orgs.find((o) => o.id === organization) - : db.orgs.find((o) => o.name === organization) +export function lookupOrg(params: PP.Org): Json { + const org = db.orgs.find((o) => o.name === params.orgName) if (!org) throw notFoundErr return org } diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 841c442f9..7488a6153 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -13,6 +13,7 @@ import { sortBySemverDesc } from '../update' import { user1 } from '../user' import { db, + lookup, lookupById, lookupDisk, lookupGlobalImage, @@ -20,7 +21,6 @@ import { lookupInstance, lookupNetworkInterface, lookupOrg, - lookupOrgV1, lookupProject, lookupSamlIdp, lookupSilo, @@ -73,7 +73,7 @@ export const handlers = makeHandlers({ throw unavailableErr } - return lookupOrgV1(params.path) + return lookup.org(params.path) }, organizationUpdate({ body, ...params }) { const org = lookupOrg(params.path) @@ -121,7 +121,7 @@ export const handlers = makeHandlers({ const { organization } = params.query if (!organization) throw notFoundErr - const org = lookupOrgV1({ organization }) + const org = lookup.org({ organization }) const projects = db.projects.filter((p) => p.organization_id === org.id) return paginated(params.query, projects) }, @@ -139,7 +139,7 @@ export const handlers = makeHandlers({ return json(newProject, { status: 201 }) }, - projectView: (params) => lookupProject(params.path), + projectViewV1: ({ path, query }) => lookup.project({ ...path, ...query }), projectUpdate({ body, ...params }) { const project = lookupProject(params.path) if (body.name) { @@ -1104,7 +1104,6 @@ export const handlers = makeHandlers({ projectPolicyUpdateV1: NotImplemented, projectPolicyViewV1: NotImplemented, projectUpdateV1: NotImplemented, - projectViewV1: NotImplemented, rackListV1: NotImplemented, rackViewV1: NotImplemented, sagaListV1: NotImplemented, @@ -1142,6 +1141,7 @@ export const handlers = makeHandlers({ // Deprecated endpoints organizationList: NotImplemented, - projectList: NotImplemented, organizationView: NotImplemented, + projectList: NotImplemented, + projectView: NotImplemented, }) diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index f84746af5..843a26c01 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -3,5 +3,5 @@ import type { Merge } from 'type-fest' export type Org = { organization: string } -export type Project = Merge -export type Instance = Merge +export type Project = Merge, { project: string }> +export type Instance = Merge, { instance: string }> From 644fbbe8eb4fa7d87468f234f90218b82e2ccea0 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 12:35:30 -0600 Subject: [PATCH 05/39] try it with more flexible looker uppers --- libs/api-mocks/msw/db.ts | 10 ++++++---- libs/api-mocks/msw/handlers.ts | 7 +------ libs/api/path-params-v1.ts | 6 +++--- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 864025114..16f41b12e 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -21,26 +21,28 @@ export const lookupById = export const lookup = { org({ organization }: PPv1.Org): Json { + if (!organization) throw notFoundErr + const org = isUuid(organization) ? db.orgs.find((o) => o.id === organization) : db.orgs.find((o) => o.name === organization) if (!org) throw notFoundErr + return org }, project(params: PPv1.Project): Json { const { project: id, ...orgParams } = params // if we have a project ID, look it up directly, otherwise call lookup org // with the other params to get an org ID, then look it up by org ID and name + if (!id) throw notFoundErr + if (isUuid(id)) { const project = db.projects.find((p) => p.id === id) if (!project) throw notFoundErr return project } - const { organization } = orgParams - if (!organization) throw notFoundErr - - const org = lookup.org({ organization }) + const org = lookup.org(orgParams) const project = db.projects.find((p) => p.organization_id === org.id && p.name === id) if (!project) throw notFoundErr diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 7488a6153..db2ae2371 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -32,7 +32,6 @@ import { lookupVpcRouter, lookupVpcRouterRoute, lookupVpcSubnet, - notFoundErr, } from './db' import { NotImplemented, @@ -117,11 +116,7 @@ export const handlers = makeHandlers({ return body }, projectListV1(params) { - // TODO: helper like requireOrgParams to do the check and throw if not - const { organization } = params.query - if (!organization) throw notFoundErr - - const org = lookup.org({ organization }) + const org = lookup.org(params.query) const projects = db.projects.filter((p) => p.organization_id === org.id) return paginated(params.query, projects) }, diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index 843a26c01..aa227af90 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -2,6 +2,6 @@ // this file import type { Merge } from 'type-fest' -export type Org = { organization: string } -export type Project = Merge, { project: string }> -export type Instance = Merge, { instance: string }> +export type Org = { organization?: string } +export type Project = Merge +export type Instance = Merge From aceba2c179bb046aeec14b5c6b3bb57ac2e49fba Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 12:59:34 -0600 Subject: [PATCH 06/39] instance view and instance nics. call sites very verbose but MSW is clean --- app/forms/instance-create.tsx | 10 ++++++++-- app/forms/network-interface-create.tsx | 4 ++-- app/forms/network-interface-edit.tsx | 4 ++-- .../instances/instance/InstancePage.tsx | 16 +++++++++++---- .../instances/instance/tabs/NetworkingTab.tsx | 20 +++++++++++++------ libs/api-mocks/msw/db.ts | 19 ++++++++++++++++++ libs/api-mocks/msw/handlers.ts | 12 +++++------ 7 files changed, 63 insertions(+), 22 deletions(-) diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index a6744783a..0cb920c56 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -85,8 +85,14 @@ export function CreateInstanceForm() { queryClient.invalidateQueries('instanceList', { path: pageParams }) // avoid the instance fetch when the instance page loads since we have the data queryClient.setQueryData( - 'instanceView', - { path: { ...pageParams, instanceName: instance.name } }, + 'instanceViewV1', + { + path: { instance: instance.name }, + query: { + project: pageParams.projectName, + organization: pageParams.orgName, + }, + }, instance ) addToast({ diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index 73335d624..f70fe677d 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -39,8 +39,8 @@ export default function CreateNetworkInterfaceForm({ const createNetworkInterface = useApiMutation('instanceNetworkInterfaceCreate', { onSuccess() { invariant(instanceName, 'instanceName is required when posting a network interface') - queryClient.invalidateQueries('instanceNetworkInterfaceList', { - path: { instanceName, projectName, orgName }, + queryClient.invalidateQueries('instanceNetworkInterfaceListV1', { + query: { instance: instanceName, project: projectName, organization: orgName }, }) onDismiss() }, diff --git a/app/forms/network-interface-edit.tsx b/app/forms/network-interface-edit.tsx index 452b58035..4a1017f32 100644 --- a/app/forms/network-interface-edit.tsx +++ b/app/forms/network-interface-edit.tsx @@ -22,8 +22,8 @@ export default function EditNetworkInterfaceForm({ const editNetworkInterface = useApiMutation('instanceNetworkInterfaceUpdate', { onSuccess() { invariant(instanceName, 'instanceName is required when posting a network interface') - queryClient.invalidateQueries('instanceNetworkInterfaceList', { - path: { orgName, projectName, instanceName }, + queryClient.invalidateQueries('instanceNetworkInterfaceListV1', { + query: { organization: orgName, project: projectName, instance: instanceName }, }) onDismiss() }, diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index dbdd78bc8..df044e24a 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -16,27 +16,35 @@ import { pb } from 'app/util/path-builder' import { useMakeInstanceActions } from '../actions' InstancePage.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('instanceView', { - path: requireInstanceParams(params), + const { instanceName, projectName, orgName } = requireInstanceParams(params) + await apiQueryClient.prefetchQuery('instanceViewV1', { + path: { instance: instanceName }, + query: { project: projectName, organization: orgName }, }) return null } export function InstancePage() { const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') + const { instanceName, projectName, orgName } = instanceParams + // TODO: helper to construct this out of the names, probably + const instanceSelector = { + path: { instance: instanceName }, + query: { project: projectName, organization: orgName }, + } const navigate = useNavigate() const queryClient = useApiQueryClient() const projectParams = pick(instanceParams, 'projectName', 'orgName') const makeActions = useMakeInstanceActions(projectParams, { onSuccess: () => { - queryClient.invalidateQueries('instanceView', { path: instanceParams }) + queryClient.invalidateQueries('instanceViewV1', instanceSelector) }, // go to project instances list since there's no more instance onDelete: () => navigate(pb.instances(projectParams)), }) - const { data: instance } = useApiQuery('instanceView', { path: instanceParams }) + const { data: instance } = useApiQuery('instanceViewV1', instanceSelector) const actions = useMemo( () => (instance ? makeActions(instance) : []), [instance, makeActions] diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 73ac49a41..b42d37a31 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -50,28 +50,36 @@ function ExternalIpsFromInstanceName({ value: primary }: { value: boolean }) { } NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { - const path = requireInstanceParams(params) + const { instanceName, projectName, orgName } = requireInstanceParams(params) + // TODO: helper to construct this out of the names, probably + const instance = { instance: instanceName } + const project = { project: projectName, organization: orgName } + await Promise.all([ - await apiQueryClient.prefetchQuery('instanceNetworkInterfaceList', { - path, - query: { limit: 10 }, + await apiQueryClient.prefetchQuery('instanceNetworkInterfaceListV1', { + query: { ...instance, ...project, limit: 10 }, }), // This is covered by the InstancePage loader but there's no downside to // being redundant. If it were removed there, we'd still want it here. - apiQueryClient.prefetchQuery('instanceView', { path }), + apiQueryClient.prefetchQuery('instanceViewV1', { path: instance, query: project }), ]) return null } export function NetworkingTab() { const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') + const { orgName, projectName, instanceName } = instanceParams + const queryClient = useApiQueryClient() const addToast = useToast() const [createModalOpen, setCreateModalOpen] = useState(false) const [editing, setEditing] = useState(null) - const getQuery = ['instanceNetworkInterfaceList', { path: instanceParams }] as const + const getQuery = [ + 'instanceNetworkInterfaceListV1', + { query: { organization: orgName, project: projectName, instance: instanceName } }, + ] as const const deleteNic = useApiMutation('instanceNetworkInterfaceDelete', { onSuccess() { diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 16f41b12e..302d1f66c 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -49,6 +49,25 @@ export const lookup = { return project }, + instance(params: PPv1.Instance): Json { + const { instance: id, ...projectParams } = params + // if we have a project ID, look it up directly, otherwise call lookup org + // with the other params to get an org ID, then look it up by org ID and name + if (!id) throw notFoundErr + + if (isUuid(id)) { + const instance = db.instances.find((p) => p.id === id) + if (!instance) throw notFoundErr + return instance + } + + const project = lookup.project(projectParams) + + const instance = db.instances.find((p) => p.project_id === project.id && p.name === id) + if (!instance) throw notFoundErr + + return instance + }, } export function lookupOrg(params: PP.Org): Json { diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index db2ae2371..0c8ccc670 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -346,7 +346,7 @@ export const handlers = makeHandlers({ db.instances.push(newInstance) return json(newInstance, { status: 201 }) }, - instanceView: (params) => lookupInstance(params.path), + instanceViewV1: ({ path, query }) => lookup.instance({ ...path, ...query }), instanceDelete(params) { const instance = lookupInstance(params.path) db.instances = db.instances.filter((i) => i.id !== instance.id) @@ -396,10 +396,10 @@ export const handlers = makeHandlers({ ], } }, - instanceNetworkInterfaceList(params) { - const instance = lookupInstance(params.path) + instanceNetworkInterfaceListV1({ query }) { + const instance = lookup.instance(query) const nics = db.networkInterfaces.filter((n) => n.instance_id === instance.id) - return paginated(params.query, nics) + return paginated(query, nics) }, instanceNetworkInterfaceCreate({ body, ...params }) { const instance = lookupInstance(params.path) @@ -1077,7 +1077,6 @@ export const handlers = makeHandlers({ instanceMigrateV1: NotImplemented, instanceNetworkInterfaceCreateV1: NotImplemented, instanceNetworkInterfaceDeleteV1: NotImplemented, - instanceNetworkInterfaceListV1: NotImplemented, instanceNetworkInterfaceUpdateV1: NotImplemented, instanceNetworkInterfaceViewV1: NotImplemented, instanceRebootV1: NotImplemented, @@ -1085,7 +1084,6 @@ export const handlers = makeHandlers({ instanceSerialConsoleV1: NotImplemented, instanceStartV1: NotImplemented, instanceStopV1: NotImplemented, - instanceViewV1: NotImplemented, organizationCreateV1: NotImplemented, organizationDeleteV1: NotImplemented, organizationPolicyUpdateV1: NotImplemented, @@ -1135,6 +1133,8 @@ export const handlers = makeHandlers({ // Deprecated endpoints + instanceView: NotImplemented, + instanceNetworkInterfaceList: NotImplemented, organizationList: NotImplemented, organizationView: NotImplemented, projectList: NotImplemented, From ed5da05d7db694b673803a93d87b2487e38fe383 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 13:16:37 -0600 Subject: [PATCH 07/39] update and delete nic --- app/forms/network-interface-edit.tsx | 5 +-- .../instances/instance/tabs/NetworkingTab.tsx | 19 +++++----- libs/api-mocks/msw/db.ts | 36 +++++++++++-------- libs/api-mocks/msw/handlers.ts | 18 +++++----- libs/api/path-params-v1.ts | 1 + 5 files changed, 46 insertions(+), 33 deletions(-) diff --git a/app/forms/network-interface-edit.tsx b/app/forms/network-interface-edit.tsx index 4a1017f32..c3d7c65ad 100644 --- a/app/forms/network-interface-edit.tsx +++ b/app/forms/network-interface-edit.tsx @@ -19,7 +19,7 @@ export default function EditNetworkInterfaceForm({ const queryClient = useApiQueryClient() const { orgName, projectName, instanceName } = useInstanceParams() - const editNetworkInterface = useApiMutation('instanceNetworkInterfaceUpdate', { + const editNetworkInterface = useApiMutation('instanceNetworkInterfaceUpdateV1', { onSuccess() { invariant(instanceName, 'instanceName is required when posting a network interface') queryClient.invalidateQueries('instanceNetworkInterfaceListV1', { @@ -40,7 +40,8 @@ export default function EditNetworkInterfaceForm({ onSubmit={(body) => { const interfaceName = defaultValues.name editNetworkInterface.mutate({ - path: { orgName, projectName, instanceName, interfaceName }, + path: { interface: interfaceName }, + query: { organization: orgName, project: projectName, instance: instanceName }, body, }) }} diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index b42d37a31..c6cdac15f 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -69,6 +69,11 @@ NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { export function NetworkingTab() { const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') const { orgName, projectName, instanceName } = instanceParams + const instanceSelector = { + organization: orgName, + project: projectName, + instance: instanceName, + } const queryClient = useApiQueryClient() const addToast = useToast() @@ -76,12 +81,9 @@ export function NetworkingTab() { const [createModalOpen, setCreateModalOpen] = useState(false) const [editing, setEditing] = useState(null) - const getQuery = [ - 'instanceNetworkInterfaceListV1', - { query: { organization: orgName, project: projectName, instance: instanceName } }, - ] as const + const getQuery = ['instanceNetworkInterfaceListV1', { query: instanceSelector }] as const - const deleteNic = useApiMutation('instanceNetworkInterfaceDelete', { + const deleteNic = useApiMutation('instanceNetworkInterfaceDeleteV1', { onSuccess() { queryClient.invalidateQueries(...getQuery) addToast({ @@ -91,7 +93,7 @@ export function NetworkingTab() { }, }) - const editNic = useApiMutation('instanceNetworkInterfaceUpdate', { + const editNic = useApiMutation('instanceNetworkInterfaceUpdateV1', { onSuccess() { queryClient.invalidateQueries(...getQuery) }, @@ -105,7 +107,8 @@ export function NetworkingTab() { label: 'Make primary', onActivate() { editNic.mutate({ - path: { ...instanceParams, interfaceName: nic.name }, + path: { interface: nic.name }, + query: instanceSelector, body: { ...nic, primary: true }, }) }, @@ -126,7 +129,7 @@ export function NetworkingTab() { { label: 'Delete', onActivate: () => { - deleteNic.mutate({ path: { ...instanceParams, interfaceName: nic.name } }) + deleteNic.mutate({ path: { interface: nic.name }, query: instanceSelector }) }, disabled: !instanceStopped && 'The instance must be stopped to delete a network interface.', diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 302d1f66c..ec3019cfe 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -63,11 +63,32 @@ export const lookup = { const project = lookup.project(projectParams) - const instance = db.instances.find((p) => p.project_id === project.id && p.name === id) + const instance = db.instances.find((i) => i.project_id === project.id && i.name === id) if (!instance) throw notFoundErr return instance }, + networkInterface(params: PPv1.NetworkInterface): Json { + const { interface: id, ...instanceParams } = params + // if we have a project ID, look it up directly, otherwise call lookup org + // with the other params to get an org ID, then look it up by org ID and name + if (!id) throw notFoundErr + + if (isUuid(id)) { + const nic = db.networkInterfaces.find((p) => p.id === id) + if (!nic) throw notFoundErr + return nic + } + + const instance = lookup.instance(instanceParams) + + const nic = db.networkInterfaces.find( + (n) => n.instance_id === instance.id && n.name === id + ) + if (!nic) throw notFoundErr + + return nic + }, } export function lookupOrg(params: PP.Org): Json { @@ -107,19 +128,6 @@ export function lookupInstance(params: PP.Instance): Json { return instance } -export function lookupNetworkInterface( - params: PP.NetworkInterface -): Json { - const instance = lookupInstance(params) - - const nic = db.networkInterfaces.find( - (n) => n.instance_id === instance.id && n.name === params.interfaceName - ) - if (!nic) throw notFoundErr - - return nic -} - export function lookupDisk(params: PP.Disk): Json { const project = lookupProject(params) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 0c8ccc670..8c39b22f2 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -19,7 +19,6 @@ import { lookupGlobalImage, lookupImage, lookupInstance, - lookupNetworkInterface, lookupOrg, lookupProject, lookupSamlIdp, @@ -435,9 +434,10 @@ export const handlers = makeHandlers({ return newNic }, - instanceNetworkInterfaceView: (params) => lookupNetworkInterface(params.path), - instanceNetworkInterfaceUpdate({ body, ...params }) { - const nic = lookupNetworkInterface(params.path) + instanceNetworkInterfaceViewV1: ({ path, query }) => + lookup.networkInterface({ ...path, ...query }), + instanceNetworkInterfaceUpdateV1({ body, path, query }) { + const nic = lookup.networkInterface({ ...path, ...query }) if (body.name) { nic.name = body.name @@ -460,8 +460,8 @@ export const handlers = makeHandlers({ return nic }, - instanceNetworkInterfaceDelete(params) { - const nic = lookupNetworkInterface(params.path) + instanceNetworkInterfaceDeleteV1({ path, query }) { + const nic = lookup.networkInterface({ ...path, ...query }) db.networkInterfaces = db.networkInterfaces.filter((n) => n.id !== nic.id) return 204 }, @@ -1076,9 +1076,6 @@ export const handlers = makeHandlers({ instanceListV1: NotImplemented, instanceMigrateV1: NotImplemented, instanceNetworkInterfaceCreateV1: NotImplemented, - instanceNetworkInterfaceDeleteV1: NotImplemented, - instanceNetworkInterfaceUpdateV1: NotImplemented, - instanceNetworkInterfaceViewV1: NotImplemented, instanceRebootV1: NotImplemented, instanceSerialConsoleStreamV1: NotImplemented, instanceSerialConsoleV1: NotImplemented, @@ -1134,7 +1131,10 @@ export const handlers = makeHandlers({ // Deprecated endpoints instanceView: NotImplemented, + instanceNetworkInterfaceDelete: NotImplemented, instanceNetworkInterfaceList: NotImplemented, + instanceNetworkInterfaceUpdate: NotImplemented, + instanceNetworkInterfaceView: NotImplemented, organizationList: NotImplemented, organizationView: NotImplemented, projectList: NotImplemented, diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index aa227af90..82bb30735 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -5,3 +5,4 @@ import type { Merge } from 'type-fest' export type Org = { organization?: string } export type Project = Merge export type Instance = Merge +export type NetworkInterface = Merge From 4c5be030fb432e834340d290f8c29fbf3485089e Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 13:20:18 -0600 Subject: [PATCH 08/39] missed a couple of instanceView calls --- .../instances/instance/tabs/NetworkingTab.tsx | 12 ++++++------ .../project/instances/instance/tabs/StorageTab.tsx | 12 ++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index c6cdac15f..40a93af48 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -69,11 +69,8 @@ NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { export function NetworkingTab() { const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') const { orgName, projectName, instanceName } = instanceParams - const instanceSelector = { - organization: orgName, - project: projectName, - instance: instanceName, - } + const projectSelector = { organization: orgName, project: projectName } + const instanceSelector = { instance: instanceName, ...projectSelector } const queryClient = useApiQueryClient() const addToast = useToast() @@ -100,7 +97,10 @@ export function NetworkingTab() { }) const instanceStopped = - useApiQuery('instanceView', { path: instanceParams }).data?.runState === 'stopped' + useApiQuery('instanceViewV1', { + path: { instance: instanceName }, + query: projectSelector, + }).data?.runState === 'stopped' const makeActions = (nic: NetworkInterface): MenuAction[] => [ { diff --git a/app/pages/project/instances/instance/tabs/StorageTab.tsx b/app/pages/project/instances/instance/tabs/StorageTab.tsx index 8edb3bb16..df4b050dd 100644 --- a/app/pages/project/instances/instance/tabs/StorageTab.tsx +++ b/app/pages/project/instances/instance/tabs/StorageTab.tsx @@ -53,11 +53,15 @@ const staticCols = [ StorageTab.loader = async ({ params }: LoaderFunctionArgs) => { const path = requireInstanceParams(params) + const { instanceName, projectName, orgName } = path await Promise.all([ apiQueryClient.prefetchQuery('instanceDiskList', { path }), // This is covered by the InstancePage loader but there's no downside to // being redundant. If it were removed there, we'd still want it here. - apiQueryClient.prefetchQuery('instanceView', { path }), + apiQueryClient.prefetchQuery('instanceViewV1', { + path: { instance: instanceName }, + query: { project: projectName, organization: orgName }, + }), ]) return null } @@ -69,13 +73,17 @@ export function StorageTab() { const addToast = useToast() const queryClient = useApiQueryClient() const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') + const { instanceName, projectName, orgName } = instanceParams const { data } = useApiQuery('instanceDiskList', { path: instanceParams }) const detachDisk = useApiMutation('instanceDiskDetach', {}) const instanceStopped = - useApiQuery('instanceView', { path: instanceParams }).data?.runState === 'stopped' + useApiQuery('instanceViewV1', { + path: { instance: instanceName }, + query: { project: projectName, organization: orgName }, + }).data?.runState === 'stopped' const makeActions = useCallback( (disk: Disk): MenuAction[] => [ From 3bd783700f41815f2bdc87d2fbf582f91b99fd23 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 16:33:13 -0600 Subject: [PATCH 09/39] notes on helpers, clean up lookups --- libs/api-mocks/msw/db.ts | 63 ++++++++++++++++---------------------- libs/api/path-params-v1.ts | 19 ++++++++++++ 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index ec3019cfe..aa4e271a5 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -1,3 +1,5 @@ +// note that isUuid checks for any kind of UUID. strictly speaking, we should +// only be checking for v4 import { validate as isUuid } from 'uuid' import * as mock from '@oxide/api-mocks' @@ -19,68 +21,55 @@ export const lookupById = return item } +// TODO: obviously lookupById2 is not the final name +export const lookupById2 = (table: T[], id: string) => { + const item = table.find((i) => i.id === id) + if (!item) throw notFoundErr + return item +} + export const lookup = { - org({ organization }: PPv1.Org): Json { - if (!organization) throw notFoundErr + org({ organization: id }: PPv1.Org): Json { + if (!id) throw notFoundErr - const org = isUuid(organization) - ? db.orgs.find((o) => o.id === organization) - : db.orgs.find((o) => o.name === organization) + if (isUuid(id)) return lookupById2(db.orgs, id) + + const org = db.orgs.find((o) => o.name === id) if (!org) throw notFoundErr return org }, - project(params: PPv1.Project): Json { - const { project: id, ...orgParams } = params - // if we have a project ID, look it up directly, otherwise call lookup org - // with the other params to get an org ID, then look it up by org ID and name + project({ project: id, ...orgSelector }: PPv1.Project): Json { if (!id) throw notFoundErr - if (isUuid(id)) { - const project = db.projects.find((p) => p.id === id) - if (!project) throw notFoundErr - return project - } - - const org = lookup.org(orgParams) + if (isUuid(id)) return lookupById2(db.projects, id) + const org = lookup.org(orgSelector) const project = db.projects.find((p) => p.organization_id === org.id && p.name === id) if (!project) throw notFoundErr return project }, - instance(params: PPv1.Instance): Json { - const { instance: id, ...projectParams } = params - // if we have a project ID, look it up directly, otherwise call lookup org - // with the other params to get an org ID, then look it up by org ID and name + instance({ instance: id, ...projectSelector }: PPv1.Instance): Json { if (!id) throw notFoundErr - if (isUuid(id)) { - const instance = db.instances.find((p) => p.id === id) - if (!instance) throw notFoundErr - return instance - } - - const project = lookup.project(projectParams) + if (isUuid(id)) return lookupById2(db.instances, id) + const project = lookup.project(projectSelector) const instance = db.instances.find((i) => i.project_id === project.id && i.name === id) if (!instance) throw notFoundErr return instance }, - networkInterface(params: PPv1.NetworkInterface): Json { - const { interface: id, ...instanceParams } = params - // if we have a project ID, look it up directly, otherwise call lookup org - // with the other params to get an org ID, then look it up by org ID and name + networkInterface({ + interface: id, + ...instanceSelector + }: PPv1.NetworkInterface): Json { if (!id) throw notFoundErr - if (isUuid(id)) { - const nic = db.networkInterfaces.find((p) => p.id === id) - if (!nic) throw notFoundErr - return nic - } + if (isUuid(id)) return lookupById2(db.networkInterfaces, id) - const instance = lookup.instance(instanceParams) + const instance = lookup.instance(instanceSelector) const nic = db.networkInterfaces.find( (n) => n.instance_id === instance.id && n.name === id diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index 82bb30735..332ec10b4 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -6,3 +6,22 @@ export type Org = { organization?: string } export type Project = Merge export type Instance = Merge export type NetworkInterface = Merge + +// notes on needed helpers: sometimes you need to select an instance with +// +// { path: { instance }, query: { project, organization } } +// +// and sometimes you need +// +// { query: { instance, project, organization } } +// +// so converting between those two forms probably makes sense. Having a name for +// each form will probably be necessary. Another thing we often need to do is +// extract a selector from the RR path params +// +// PathBuilder probably also needs to change to take the selector form instead +// of the names +// +// we may also want to change the names of the params in the URL? on the other +// hand it's nice to be explcit that they're names if that's what they are, and +// the helpers will make it tolerable From da4a34f14594658c985d80278e9fc97b1a9d5c7a Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 16:37:50 -0600 Subject: [PATCH 10/39] project create v1 --- app/forms/project-create.tsx | 4 ++-- libs/api-mocks/msw/handlers.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index 8605a391f..12ef06ed0 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -23,7 +23,7 @@ export function CreateProjectSideModalForm() { const onDismiss = () => navigate(pb.projects({ orgName })) - const createProject = useApiMutation('projectCreate', { + const createProject = useApiMutation('projectCreateV1', { onSuccess(project) { // refetch list of projects in sidebar queryClient.invalidateQueries('projectListV1', { query: { organization: orgName } }) @@ -54,7 +54,7 @@ export function CreateProjectSideModalForm() { onDismiss={onDismiss} onSubmit={({ name, description }) => { createProject.mutate({ - path: { orgName }, + query: { organization: orgName }, body: { name, description }, }) }} diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 8c39b22f2..5d01beb2e 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -119,8 +119,8 @@ export const handlers = makeHandlers({ const projects = db.projects.filter((p) => p.organization_id === org.id) return paginated(params.query, projects) }, - projectCreate({ body, ...params }) { - const org = lookupOrg(params.path) + projectCreateV1({ body, query }) { + const org = lookup.org(query) errIfExists(db.projects, { name: body.name, organization_id: org.id }) const newProject: Json = { @@ -1089,7 +1089,6 @@ export const handlers = makeHandlers({ physicalDiskListV1: NotImplemented, policyUpdateV1: NotImplemented, policyViewV1: NotImplemented, - projectCreateV1: NotImplemented, projectDeleteV1: NotImplemented, projectPolicyUpdateV1: NotImplemented, projectPolicyViewV1: NotImplemented, @@ -1130,13 +1129,14 @@ export const handlers = makeHandlers({ // Deprecated endpoints - instanceView: NotImplemented, instanceNetworkInterfaceDelete: NotImplemented, instanceNetworkInterfaceList: NotImplemented, instanceNetworkInterfaceUpdate: NotImplemented, instanceNetworkInterfaceView: NotImplemented, + instanceView: NotImplemented, organizationList: NotImplemented, organizationView: NotImplemented, + projectCreate: NotImplemented, projectList: NotImplemented, projectView: NotImplemented, }) From 091d44d979b0027a989cbe518d0a689e792d4fb7 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 16:56:45 -0600 Subject: [PATCH 11/39] fix object already exists error message for v1 creates, hook test --- libs/api/__tests__/hooks.spec.tsx | 6 ++--- .../{nav-to-login.ts => nav-to-login.spec.ts} | 0 libs/api/errors.ts | 25 ++++++++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) rename libs/api/__tests__/{nav-to-login.ts => nav-to-login.spec.ts} (100%) diff --git a/libs/api/__tests__/hooks.spec.tsx b/libs/api/__tests__/hooks.spec.tsx index 6955fff68..2bf2baa8a 100644 --- a/libs/api/__tests__/hooks.spec.tsx +++ b/libs/api/__tests__/hooks.spec.tsx @@ -178,12 +178,12 @@ describe('useApiMutation', () => { describe('on error response', () => { const projectPost404Params = { - path: { orgName: 'nonexistent' }, + query: { organization: 'nonexistent' }, body: { name: 'will-fail', description: '' }, } it('passes through raw response', async () => { - const { result } = renderHook(() => useApiMutation('projectCreate'), config) + const { result } = renderHook(() => useApiMutation('projectCreateV1'), config) act(() => result.current.mutate(projectPost404Params)) @@ -194,7 +194,7 @@ describe('useApiMutation', () => { }) it('parses error json if possible', async () => { - const { result } = renderHook(() => useApiMutation('projectCreate'), config) + const { result } = renderHook(() => useApiMutation('projectCreateV1'), config) act(() => result.current.mutate(projectPost404Params)) diff --git a/libs/api/__tests__/nav-to-login.ts b/libs/api/__tests__/nav-to-login.spec.ts similarity index 100% rename from libs/api/__tests__/nav-to-login.ts rename to libs/api/__tests__/nav-to-login.spec.ts diff --git a/libs/api/errors.ts b/libs/api/errors.ts index 5d5c79261..05bcdf133 100644 --- a/libs/api/errors.ts +++ b/libs/api/errors.ts @@ -2,6 +2,14 @@ import { camelCaseToWords, capitalize } from '@oxide/util' import type { ErrorBody, ErrorResult } from '.' +// assume a nice short resource name is the word before create +export function getResourceName(method: string) { + const words = camelCaseToWords(method) + const i = words.indexOf('create') + if (i < 1) return null + return words[i - 1].replace(/s$/, '') +} + const msgFromCode = ( method: string, errorCode: string, @@ -12,12 +20,13 @@ const msgFromCode = ( return 'Action not authorized' // TODO: This is a temporary fix for the API; better messages should be provided from there - case 'ObjectAlreadyExists': - if (method.endsWith('Create')) { - const resource = camelCaseToWords(method).slice(-2)[0].replace(/s$/, '') + case 'ObjectAlreadyExists': { + const resource = getResourceName(method) + if (resource) { return `${capitalize(resource)} name already exists` } return undefined + } default: return undefined } @@ -128,4 +137,14 @@ if (import.meta.vitest) { expect(formatServerError('', alreadyExists()).error.message).toEqual('whatever') }) }) + + it.each([ + ['projectCreate', 'project'], + ['projectCreateV1', 'project'], + ['instanceNetworkInterfaceCreate', 'interface'], + ['instanceNetworkInterfaceCreateV1', 'interface'], + ['doesNotContainC-reate', null], + ])('getResourceName gets resource names', (method, resource) => { + expect(getResourceName(method)).toEqual(resource) + }) } From 316d263348d66b38ff198132cb31f172213eea8b Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 17:04:19 -0600 Subject: [PATCH 12/39] project update v1 --- app/forms/project-edit.tsx | 5 +++-- libs/api-mocks/msw/handlers.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index d1b5dd2e0..cf39d71c5 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -32,7 +32,7 @@ export function EditProjectSideModalForm() { query: { organization: orgName }, }) - const editProject = useApiMutation('projectUpdate', { + const editProject = useApiMutation('projectUpdateV1', { onSuccess(project) { // refetch list of projects in sidebar // TODO: check this invalidation @@ -63,7 +63,8 @@ export function EditProjectSideModalForm() { onDismiss={onDismiss} onSubmit={({ name, description }) => { editProject.mutate({ - path: { orgName, projectName }, + path: { project: projectName }, + query: { organization: orgName }, body: { name, description }, }) }} diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 5d01beb2e..f02266d50 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -134,8 +134,8 @@ export const handlers = makeHandlers({ return json(newProject, { status: 201 }) }, projectViewV1: ({ path, query }) => lookup.project({ ...path, ...query }), - projectUpdate({ body, ...params }) { - const project = lookupProject(params.path) + projectUpdateV1({ body, path, query }) { + const project = lookup.project({ ...path, ...query }) if (body.name) { project.name = body.name } @@ -1092,7 +1092,6 @@ export const handlers = makeHandlers({ projectDeleteV1: NotImplemented, projectPolicyUpdateV1: NotImplemented, projectPolicyViewV1: NotImplemented, - projectUpdateV1: NotImplemented, rackListV1: NotImplemented, rackViewV1: NotImplemented, sagaListV1: NotImplemented, @@ -1138,5 +1137,6 @@ export const handlers = makeHandlers({ organizationView: NotImplemented, projectCreate: NotImplemented, projectList: NotImplemented, + projectUpdate: NotImplemented, projectView: NotImplemented, }) From 31a0700e04715e32556fac97c5ff2723cff1d3d7 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Sat, 18 Feb 2023 17:06:36 -0600 Subject: [PATCH 13/39] project delete v1 --- app/pages/ProjectsPage.tsx | 7 +++++-- libs/api-mocks/msw/handlers.ts | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index e8ec7d0cd..399e3052d 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -55,7 +55,7 @@ export default function ProjectsPage() { }, }) - const deleteProject = useApiMutation('projectDelete', { + const deleteProject = useApiMutation('projectDeleteV1', { onSuccess() { // TODO: figure out if this is invalidating as expected, can we leave out the query // altogether, etc. Look at whether limit param matters. @@ -84,7 +84,10 @@ export default function ProjectsPage() { { label: 'Delete', onActivate: () => { - deleteProject.mutate({ path: { orgName, projectName: project.name } }) + deleteProject.mutate({ + path: { project: project.name }, + query: { organization: orgName }, + }) }, }, ] diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index f02266d50..4534948a4 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -143,8 +143,8 @@ export const handlers = makeHandlers({ return project }, - projectDelete(params) { - const project = lookupProject(params.path) + projectDeleteV1({ path, query }) { + const project = lookup.project({ ...path, ...query }) db.projects = db.projects.filter((p) => p.id !== project.id) @@ -1089,7 +1089,6 @@ export const handlers = makeHandlers({ physicalDiskListV1: NotImplemented, policyUpdateV1: NotImplemented, policyViewV1: NotImplemented, - projectDeleteV1: NotImplemented, projectPolicyUpdateV1: NotImplemented, projectPolicyViewV1: NotImplemented, rackListV1: NotImplemented, @@ -1136,6 +1135,7 @@ export const handlers = makeHandlers({ organizationList: NotImplemented, organizationView: NotImplemented, projectCreate: NotImplemented, + projectDelete: NotImplemented, projectList: NotImplemented, projectUpdate: NotImplemented, projectView: NotImplemented, From 05db7ac8a33053677fe820c47e54cc65cea238b5 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 20 Feb 2023 11:51:47 -0600 Subject: [PATCH 14/39] attempt at "helpers" that are supposed to "help" but are actually "shit" --- app/forms/network-interface-edit.tsx | 13 ++-- app/forms/project-create.tsx | 7 +- app/forms/project-edit.tsx | 29 ++++--- app/hooks/use-params.ts | 6 ++ app/pages/ProjectsPage.tsx | 16 ++-- .../instances/instance/InstancePage.tsx | 24 +++--- .../instances/instance/tabs/NetworkingTab.tsx | 25 ++++--- libs/api-mocks/json-type.type-spec.ts | 22 +++--- libs/api/index.ts | 2 + libs/api/path-params-v1.ts | 19 ----- libs/api/selector.spec.ts | 51 +++++++++++++ libs/api/selector.ts | 75 +++++++++++++++++++ 12 files changed, 208 insertions(+), 81 deletions(-) create mode 100644 libs/api/selector.spec.ts create mode 100644 libs/api/selector.ts diff --git a/app/forms/network-interface-edit.tsx b/app/forms/network-interface-edit.tsx index c3d7c65ad..a201fa010 100644 --- a/app/forms/network-interface-edit.tsx +++ b/app/forms/network-interface-edit.tsx @@ -5,7 +5,7 @@ import { useApiMutation, useApiQueryClient } from '@oxide/api' import { pick } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { useInstanceParams } from 'app/hooks' +import { useInstanceSelector } from 'app/hooks' type EditNetworkInterfaceFormProps = { editing: NetworkInterface @@ -17,13 +17,16 @@ export default function EditNetworkInterfaceForm({ editing, }: EditNetworkInterfaceFormProps) { const queryClient = useApiQueryClient() - const { orgName, projectName, instanceName } = useInstanceParams() + const instanceSelector = useInstanceSelector() const editNetworkInterface = useApiMutation('instanceNetworkInterfaceUpdateV1', { onSuccess() { - invariant(instanceName, 'instanceName is required when posting a network interface') + invariant( + instanceSelector.instance, + 'instanceName is required when posting a network interface' + ) queryClient.invalidateQueries('instanceNetworkInterfaceListV1', { - query: { organization: orgName, project: projectName, instance: instanceName }, + query: instanceSelector, }) onDismiss() }, @@ -41,7 +44,7 @@ export default function EditNetworkInterfaceForm({ const interfaceName = defaultValues.name editNetworkInterface.mutate({ path: { interface: interfaceName }, - query: { organization: orgName, project: projectName, instance: instanceName }, + query: instanceSelector, body, }) }} diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index 12ef06ed0..732c79227 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -1,7 +1,7 @@ import { useNavigate } from 'react-router-dom' import type { ProjectCreate } from '@oxide/api' -import { useApiMutation, useApiQueryClient } from '@oxide/api' +import { toApiSelector, toPathQuery, useApiMutation, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' @@ -31,10 +31,7 @@ export function CreateProjectSideModalForm() { const projectParams = { orgName, projectName: project.name } queryClient.setQueryData( 'projectViewV1', - { - path: { project: project.name }, - query: { organization: orgName }, - }, + toPathQuery('project', toApiSelector(projectParams)), project ) addToast({ diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index cf39d71c5..6e1eab8e3 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -1,13 +1,20 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { useNavigate } from 'react-router-dom' -import { apiQueryClient, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' +import { + apiQueryClient, + toApiSelector, + toPathQuery, + useApiMutation, + useApiQuery, + useApiQueryClient, +} from '@oxide/api' import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { pb } from 'app/util/path-builder' -import { requireProjectParams, useRequiredParams, useToast } from '../hooks' +import { requireProjectParams, useProjectParams, useToast } from '../hooks' EditProjectSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { const { projectName, orgName } = requireProjectParams(params) @@ -23,14 +30,19 @@ export function EditProjectSideModalForm() { const addToast = useToast() const navigate = useNavigate() - const { orgName, projectName } = useRequiredParams('orgName', 'projectName') + const projectParams = useProjectParams() + const { orgName } = projectParams const onDismiss = () => navigate(pb.projects({ orgName })) - const { data: project } = useApiQuery('projectViewV1', { - path: { project: projectName }, - query: { organization: orgName }, - }) + const { data: project } = useApiQuery( + 'projectViewV1', + // ok, I immediately feel this is a bad idea and want to change course. too + // many function calls. type inference on hover helps show what you're doing + // but it's still very alienating from the simple objects actually being + // passed around + toPathQuery('project', toApiSelector(projectParams)) + ) const editProject = useApiMutation('projectUpdateV1', { onSuccess(project) { @@ -63,8 +75,7 @@ export function EditProjectSideModalForm() { onDismiss={onDismiss} onSubmit={({ name, description }) => { editProject.mutate({ - path: { project: projectName }, - query: { organization: orgName }, + ...toPathQuery('project', toApiSelector(projectParams)), body: { name, description }, }) }} diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index 6e0dce369..70b25456b 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -2,6 +2,8 @@ import type { Params } from 'react-router-dom' import { useParams } from 'react-router-dom' import invariant from 'tiny-invariant' +import { toApiSelector } from '@oxide/api' + const err = (param: string) => `Param '${param}' not found in route. You might be rendering a component under the wrong route.` @@ -35,6 +37,10 @@ export const useSiloParams = () => requireSiloParams(useParams()) export const useSledParams = () => requireSledParams(useParams()) export const useUpdateParams = () => requireUpdateParams(useParams()) +export const useOrgSelector = () => toApiSelector(useOrgParams()) +export const useProjectSelector = () => toApiSelector(useProjectParams()) +export const useInstanceSelector = () => toApiSelector(useInstanceParams()) + /** * 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 diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index 399e3052d..e47aedcc5 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -19,7 +19,7 @@ import { import { pb } from 'app/util/path-builder' -import { requireOrgParams, useOrgParams, useQuickActions } from '../hooks' +import { requireOrgParams, useOrgParams, useOrgSelector, useQuickActions } from '../hooks' const EmptyState = () => ( { deleteProject.mutate({ path: { project: project.name }, - query: { organization: orgName }, + query: orgSelector, }) }, }, diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index df044e24a..4de5a77a7 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -3,7 +3,13 @@ import { useMemo } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' import { useNavigate } from 'react-router-dom' -import { apiQueryClient, useApiQuery, useApiQueryClient } from '@oxide/api' +import { + apiQueryClient, + toApiSelector, + toPathQuery, + useApiQuery, + useApiQueryClient, +} from '@oxide/api' import { Instances24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui' import { pick } from '@oxide/util' @@ -16,22 +22,16 @@ import { pb } from 'app/util/path-builder' import { useMakeInstanceActions } from '../actions' InstancePage.loader = async ({ params }: LoaderFunctionArgs) => { - const { instanceName, projectName, orgName } = requireInstanceParams(params) - await apiQueryClient.prefetchQuery('instanceViewV1', { - path: { instance: instanceName }, - query: { project: projectName, organization: orgName }, - }) + await apiQueryClient.prefetchQuery( + 'instanceViewV1', + toPathQuery('instance', toApiSelector(requireInstanceParams(params))) + ) return null } export function InstancePage() { const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') - const { instanceName, projectName, orgName } = instanceParams - // TODO: helper to construct this out of the names, probably - const instanceSelector = { - path: { instance: instanceName }, - query: { project: projectName, organization: orgName }, - } + const instanceSelector = toPathQuery('instance', toApiSelector(instanceParams)) const navigate = useNavigate() const queryClient = useApiQueryClient() diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 40a93af48..8b99a0739 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -3,8 +3,14 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { Link } from 'react-router-dom' import type { NetworkInterface } from '@oxide/api' -import { apiQueryClient } from '@oxide/api' -import { useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' +import { + apiQueryClient, + toApiSelector, + toPathQuery, + useApiMutation, + useApiQuery, + useApiQueryClient, +} from '@oxide/api' import type { MenuAction } from '@oxide/table' import { useQueryTable } from '@oxide/table' import { @@ -50,18 +56,17 @@ function ExternalIpsFromInstanceName({ value: primary }: { value: boolean }) { } NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { - const { instanceName, projectName, orgName } = requireInstanceParams(params) - // TODO: helper to construct this out of the names, probably - const instance = { instance: instanceName } - const project = { project: projectName, organization: orgName } - + const instanceSelector = toApiSelector(requireInstanceParams(params)) await Promise.all([ - await apiQueryClient.prefetchQuery('instanceNetworkInterfaceListV1', { - query: { ...instance, ...project, limit: 10 }, + apiQueryClient.prefetchQuery('instanceNetworkInterfaceListV1', { + query: { ...instanceSelector, limit: 10 }, }), // This is covered by the InstancePage loader but there's no downside to // being redundant. If it were removed there, we'd still want it here. - apiQueryClient.prefetchQuery('instanceViewV1', { path: instance, query: project }), + apiQueryClient.prefetchQuery( + 'instanceViewV1', + toPathQuery('instance', instanceSelector) + ), ]) return null } diff --git a/libs/api-mocks/json-type.type-spec.ts b/libs/api-mocks/json-type.type-spec.ts index d5a391d81..a012beca5 100644 --- a/libs/api-mocks/json-type.type-spec.ts +++ b/libs/api-mocks/json-type.type-spec.ts @@ -1,26 +1,26 @@ +import { assertType } from 'vitest' + import type { VpcSubnet } from '@oxide/api' import type { Json } from './json-type' -// Tests of a sort. These expectType calls will fail to typecheck if the types +// Tests of a sort. These assertType calls will fail to typecheck if the types // are not equal. There's no point in wrapping this in a real test because it // will always pass. -const expectType = (_value: T) => {} - let val: any // eslint-disable-line @typescript-eslint/no-explicit-any // just checking :) -expectType<1>(val as 1) +assertType<1>(val as 1) // @ts-expect-error -expectType<1>(val as 2) +assertType<1>(val as 2) // @ts-expect-error -expectType<{ x: string }>(val as { x: number }) +assertType<{ x: string }>(val as { x: number }) -expectType(val as Json) -expectType(val as Json) -expectType<{ x: string; y: number }>(val as Json<{ x: Date; y: number }>) -expectType<{ x: { a_b45_c: string }; z: string[] }>( +assertType(val as Json) +assertType(val as Json) +assertType<{ x: string; y: number }>(val as Json<{ x: Date; y: number }>) +assertType<{ x: { a_b45_c: string }; z: string[] }>( val as Json<{ x: { aB45C: Date }; z: Date[] }> ) @@ -35,4 +35,4 @@ type VpcSubnetJSON = { vpc_id: string } -expectType(val as Json) +assertType(val as Json) diff --git a/libs/api/index.ts b/libs/api/index.ts index 1c8295c8a..2f2b89b54 100644 --- a/libs/api/index.ts +++ b/libs/api/index.ts @@ -50,3 +50,5 @@ export * as PathParamsV1 from './path-params-v1' export type { Params, Result, ResultItem } from './hooks' export { navToLogin } from './nav-to-login' + +export * from './selector' diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index 332ec10b4..82bb30735 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -6,22 +6,3 @@ export type Org = { organization?: string } export type Project = Merge export type Instance = Merge export type NetworkInterface = Merge - -// notes on needed helpers: sometimes you need to select an instance with -// -// { path: { instance }, query: { project, organization } } -// -// and sometimes you need -// -// { query: { instance, project, organization } } -// -// so converting between those two forms probably makes sense. Having a name for -// each form will probably be necessary. Another thing we often need to do is -// extract a selector from the RR path params -// -// PathBuilder probably also needs to change to take the selector form instead -// of the names -// -// we may also want to change the names of the params in the URL? on the other -// hand it's nice to be explcit that they're names if that's what they are, and -// the helpers will make it tolerable diff --git a/libs/api/selector.spec.ts b/libs/api/selector.spec.ts new file mode 100644 index 000000000..543b4ad5a --- /dev/null +++ b/libs/api/selector.spec.ts @@ -0,0 +1,51 @@ +import { assertType } from 'vitest' + +import { toApiSelector, toPathQuery } from './selector' + +describe('toPathQuery', () => { + it('works in the base case', () => { + const result = toPathQuery('instance', { + instance: 'i', + project: 'p', + organization: 'o', + }) + expect(result).toEqual({ + path: { instance: 'i' }, + query: { project: 'p', organization: 'o' }, + }) + + // with nice type inference + assertType<{ + path: { instance: string } + query: { project: string; organization: string } + }>(result) + }) + + it('leaves an empty query in there when there is only the one key', () => { + expect(toPathQuery('instance', { instance: 'i' })).toEqual({ + path: { instance: 'i' }, + query: {}, + }) + }) + + it('type errors on missing key', () => { + // type error if key is not in the object + // @ts-expect-error + toPathQuery('instance', { instanc: 'i', project: 'p', organization: 'o' }) + }) +}) + +describe('toApiSelector', () => { + it('converts xName to x, handling orgName specially', () => { + const result = toApiSelector({ orgName: 'abc', projectName: 'def' }) + expect(result).toEqual({ organization: 'abc', project: 'def' }) + + // make sure it gets the type right + assertType<{ organization: string; project: string }>(result) + }) + + it('type errors on keys that do not end in Name', () => { + // @ts-expect-error keys must end in 'Name' + toApiSelector({ projectNam: 'abc' }) + }) +}) diff --git a/libs/api/selector.ts b/libs/api/selector.ts new file mode 100644 index 000000000..f158507db --- /dev/null +++ b/libs/api/selector.ts @@ -0,0 +1,75 @@ +import type { Replace } from 'type-fest' + +import { exclude } from '@oxide/util' + +// notes on needed helpers: sometimes you need to select an instance with +// +// { path: { instance }, query: { project, organization } } +// +// and sometimes you need +// +// { query: { instance, project, organization } } +// +// so converting between those two forms probably makes sense. Having a name for +// each form will probably be necessary. Another thing we often need to do is +// extract a selector from the RR path params +// +// PathBuilder probably also needs to change to take the selector form instead +// of the names +// +// We may also want to change the names of the params in the URL? on the other +// hand it's nice to be explcit that they're names if that's what they are, and +// the helpers will make it tolerable. If we plan on keeping the route structure +// in the console, then maybe we actually want the helpers to go the other way: +// the canonical form is `{ orgName, projectName, instanceName }`, and we have +// helpers `toQuery` and `toPathQuery` for converting to the API forms. In order +// to do this cleanly, we should probably make it so the names can be converted +// trivially by stripping `-Name` off the end. Does this mean we need to change +// `orgName` to `organizationName`? That would be huge headache so it might be +// better to handle that one specially for now. However, that messes up the types. + +/** + * Convert a selector to API params with one key in `path` and the rest in + * `query`. To add extra query params like `limit`, just include them in + * `selector` and they'll end up in `query` like everything else. + * + * Mapped types are hard to read here, but they give the calling code nice + * inference. + */ +export const toPathQuery = ( + pathKey: PK, + selector: Record +) => ({ + path: { [pathKey]: selector[pathKey] } as { [K0 in PK]: string }, + query: exclude(selector, pathKey) as { [K0 in Exclude]: string }, +}) + +type StripName = K extends `${infer K0}Name` ? K0 : K + +/** + * Turn + * + * ```ts + * { orgName: 'abc', projectName: 'def' } + * ``` + * into + * ```ts + * { organization: 'abc', project: 'def' } + * ``` + * + * while maintaining type-level awareness of keys. Note special handling of + * `orgName` to avoid having to convert hundreds of lines of existing code to + * use `organizationName`. Organizations are going to disappear anyway, which + * will make this unnecessary. + */ +export function toApiSelector(selector: Record) { + return Object.fromEntries( + Object.entries(selector).map(([k, v]) => [ + k === 'orgName' ? 'organization' : stripName(k as K), + v, + ]) + ) as { [K1 in StripName>]: string } +} + +const stripName = (nameKey: `${K}Name`) => + nameKey.replace(/Name$/, '') as StripName From b86700b1350fdc955133816b04fb16b26795d0ca Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 20 Feb 2023 13:02:34 -0600 Subject: [PATCH 15/39] instance disk attach/detach, wrap of instance disk list --- app/forms/disk-attach.tsx | 24 +++++++--- .../instances/instance/tabs/MetricsTab.tsx | 21 +++++---- .../instances/instance/tabs/StorageTab.tsx | 47 ++++++++++--------- libs/api-mocks/msw/db.ts | 12 +++++ libs/api-mocks/msw/handlers.ts | 29 ++++++------ libs/api/path-params-v1.ts | 1 + 6 files changed, 80 insertions(+), 54 deletions(-) diff --git a/app/forms/disk-attach.tsx b/app/forms/disk-attach.tsx index 0dcaf8f83..d59642b1e 100644 --- a/app/forms/disk-attach.tsx +++ b/app/forms/disk-attach.tsx @@ -1,7 +1,13 @@ import invariant from 'tiny-invariant' import type { Disk, DiskIdentifier } from '@oxide/api' -import { useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' +import { + toApiSelector, + toPathQuery, + useApiMutation, + useApiQuery, + useApiQueryClient, +} from '@oxide/api' import { ListboxField, SideModalForm } from 'app/components/form' import { useAllParams } from 'app/hooks' @@ -23,12 +29,13 @@ export function AttachDiskSideModalForm({ const queryClient = useApiQueryClient() const { orgName, projectName, instanceName } = useAllParams('orgName', 'projectName') - const attachDisk = useApiMutation('instanceDiskAttach', { + const attachDisk = useApiMutation('instanceDiskAttachV1', { onSuccess(data) { invariant(instanceName, 'instanceName is required') - queryClient.invalidateQueries('instanceDiskList', { - path: { orgName, projectName, instanceName }, - }) + queryClient.invalidateQueries( + 'instanceDiskListV1', + toPathQuery('instance', toApiSelector({ orgName, projectName, instanceName })) + ) onSuccess?.(data) onDismiss() }, @@ -53,8 +60,11 @@ export function AttachDiskSideModalForm({ (({ name }) => { invariant(instanceName, 'instanceName is required') attachDisk.mutate({ - path: { orgName, projectName, instanceName }, - body: { name }, + ...toPathQuery( + 'instance', + toApiSelector({ orgName, projectName, instanceName }) + ), + body: { disk: name }, }) }) } diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index e92fb4859..9fef29f37 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -1,12 +1,10 @@ import { getLocalTimeZone } from '@internationalized/date' -import { Suspense, useMemo, useState } from 'react' -import React from 'react' +import React, { Suspense, useMemo, useState } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' import invariant from 'tiny-invariant' import type { Cumulativeint64, DiskMetricName } from '@oxide/api' -import { apiQueryClient } from '@oxide/api' -import { useApiQuery } from '@oxide/api' +import { apiQueryClient, toApiSelector, toPathQuery, useApiQuery } from '@oxide/api' import { Listbox, Spinner } from '@oxide/ui' import { useDateTimeRangePicker } from 'app/components/form' @@ -76,22 +74,27 @@ function DiskMetric({ // date range, I'm inclined to punt. MetricsTab.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('instanceDiskList', { - path: requireInstanceParams(params), - }) + const instanceParams = requireInstanceParams(params) + await apiQueryClient.prefetchQuery( + 'instanceDiskListV1', + toPathQuery('instance', toApiSelector(instanceParams)) + ) return null } export function MetricsTab() { const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') - const { data } = useApiQuery('instanceDiskList', { path: instanceParams }) + const { data } = useApiQuery( + 'instanceDiskListV1', + toPathQuery('instance', toApiSelector(instanceParams)) + ) const disks = useMemo(() => data?.items || [], [data]) // because of prefetch in the loader and because an instance should always // have a disk, we should never see an empty list here invariant(disks.length > 0, 'Instance disks list should never be empty') - const { orgName, projectName } = useRequiredParams('orgName', 'projectName') + const { orgName, projectName } = instanceParams const { startTime, endTime, dateTimeRangePicker } = useDateTimeRangePicker('lastDay') diff --git a/app/pages/project/instances/instance/tabs/StorageTab.tsx b/app/pages/project/instances/instance/tabs/StorageTab.tsx index df4b050dd..855a4ca08 100644 --- a/app/pages/project/instances/instance/tabs/StorageTab.tsx +++ b/app/pages/project/instances/instance/tabs/StorageTab.tsx @@ -1,9 +1,15 @@ import { useCallback, useMemo, useState } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' -import type { Disk } from '@oxide/api' -import { apiQueryClient } from '@oxide/api' -import { useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' +import { + type Disk, + apiQueryClient, + toApiSelector, + toPathQuery, + useApiMutation, + useApiQuery, + useApiQueryClient, +} from '@oxide/api' import type { MenuAction } from '@oxide/table' import { DateCell, @@ -52,16 +58,13 @@ const staticCols = [ ] StorageTab.loader = async ({ params }: LoaderFunctionArgs) => { - const path = requireInstanceParams(params) - const { instanceName, projectName, orgName } = path + const instanceParams = requireInstanceParams(params) + const instanceSelector = toPathQuery('instance', toApiSelector(instanceParams)) await Promise.all([ - apiQueryClient.prefetchQuery('instanceDiskList', { path }), + apiQueryClient.prefetchQuery('instanceDiskListV1', instanceSelector), // This is covered by the InstancePage loader but there's no downside to // being redundant. If it were removed there, we'd still want it here. - apiQueryClient.prefetchQuery('instanceViewV1', { - path: { instance: instanceName }, - query: { project: projectName, organization: orgName }, - }), + apiQueryClient.prefetchQuery('instanceViewV1', instanceSelector), ]) return null } @@ -73,17 +76,14 @@ export function StorageTab() { const addToast = useToast() const queryClient = useApiQueryClient() const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') - const { instanceName, projectName, orgName } = instanceParams + const instanceSelector = toPathQuery('instance', toApiSelector(instanceParams)) - const { data } = useApiQuery('instanceDiskList', { path: instanceParams }) + const { data } = useApiQuery('instanceDiskListV1', instanceSelector) - const detachDisk = useApiMutation('instanceDiskDetach', {}) + const detachDisk = useApiMutation('instanceDiskDetachV1', {}) const instanceStopped = - useApiQuery('instanceViewV1', { - path: { instance: instanceName }, - query: { project: projectName, organization: orgName }, - }).data?.runState === 'stopped' + useApiQuery('instanceViewV1', instanceSelector).data?.runState === 'stopped' const makeActions = useCallback( (disk: Disk): MenuAction[] => [ @@ -93,22 +93,23 @@ export function StorageTab() { !instanceStopped && 'Instance must be stopped before disk can be detached', onActivate() { detachDisk.mutate( - { body: { name: disk.name }, path: instanceParams }, + { body: { disk: disk.name }, ...instanceSelector }, { onSuccess: () => { - queryClient.invalidateQueries('instanceDiskList', { path: instanceParams }) + queryClient.invalidateQueries('instanceDiskListV1', instanceSelector) }, } ) }, }, ], - [detachDisk, instanceParams, instanceStopped, queryClient] + [detachDisk, instanceStopped, queryClient, instanceSelector] ) - const attachDisk = useApiMutation('instanceDiskAttach', { + const attachDisk = useApiMutation('instanceDiskAttachV1', { onSuccess() { - queryClient.invalidateQueries('instanceDiskList', { path: instanceParams }) + console.log('disk atttach success') + queryClient.invalidateQueries('instanceDiskListV1', instanceSelector) }, onError(err) { addToast({ @@ -190,7 +191,7 @@ export function StorageTab() { onSuccess={({ name }) => { // TODO: this should probably be done with `mutateAsync` and // awaited, but it's a pain, so punt for now - attachDisk.mutate({ path: instanceParams, body: { name } }) + attachDisk.mutate({ ...instanceSelector, body: { disk: name } }) }} /> )} diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index aa4e271a5..03bd6f2d2 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -78,6 +78,18 @@ export const lookup = { return nic }, + disk({ disk: id, ...projectSelector }: PPv1.Disk): Json { + if (!id) throw notFoundErr + + if (isUuid(id)) return lookupById2(db.disks, id) + + const project = lookup.project(projectSelector) + + const disk = db.disks.find((d) => d.project_id === project.id && d.name === id) + if (!disk) throw notFoundErr + + return disk + }, } export function lookupOrg(params: PP.Org): Json { diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 4534948a4..1846cba47 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -351,35 +351,34 @@ export const handlers = makeHandlers({ db.instances = db.instances.filter((i) => i.id !== instance.id) return 204 }, - instanceDiskList(params) { - const instance = lookupInstance(params.path) + instanceDiskListV1({ path, query }) { + const instance = lookup.instance({ ...path, ...query }) // TODO: Should disk instance state be `instance_id` instead of `instance`? const disks = db.disks.filter( (d) => 'instance' in d.state && d.state.instance === instance.id ) - return paginated(params.query, disks) + return paginated(query, disks) }, - instanceDiskAttach({ body, ...params }) { - const instance = lookupInstance(params.path) + instanceDiskAttachV1({ body, path, query: projectParams }) { + const instance = lookup.instance({ ...path, ...projectParams }) if (instance.run_state !== 'stopped') { throw 'Cannot attach disk to instance that is not stopped' } - const disk = lookupDisk({ ...params.path, diskName: body.name }) + const disk = lookup.disk({ ...projectParams, disk: body.disk }) disk.state = { state: 'attached', instance: instance.id, } + console.log(disk) return disk }, - instanceDiskDetach({ body, ...params }) { - const instance = lookupInstance(params.path) + instanceDiskDetachV1({ body, path, query: projectParams }) { + const instance = lookup.instance({ ...path, ...projectParams }) if (instance.run_state !== 'stopped') { throw 'Cannot detach disk to instance that is not stopped' } - const disk = lookupDisk({ ...params.path, diskName: body.name }) - disk.state = { - state: 'detached', - } + const disk = lookup.disk({ ...projectParams, disk: body.disk }) + disk.state = { state: 'detached' } return disk }, instanceExternalIpList(params) { @@ -1070,9 +1069,6 @@ export const handlers = makeHandlers({ diskViewV1: NotImplemented, instanceCreateV1: NotImplemented, instanceDeleteV1: NotImplemented, - instanceDiskAttachV1: NotImplemented, - instanceDiskDetachV1: NotImplemented, - instanceDiskListV1: NotImplemented, instanceListV1: NotImplemented, instanceMigrateV1: NotImplemented, instanceNetworkInterfaceCreateV1: NotImplemented, @@ -1127,6 +1123,9 @@ export const handlers = makeHandlers({ // Deprecated endpoints + instanceDiskAttach: NotImplemented, + instanceDiskDetach: NotImplemented, + instanceDiskList: NotImplemented, instanceNetworkInterfaceDelete: NotImplemented, instanceNetworkInterfaceList: NotImplemented, instanceNetworkInterfaceUpdate: NotImplemented, diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index 82bb30735..06494d86c 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -5,4 +5,5 @@ import type { Merge } from 'type-fest' export type Org = { organization?: string } export type Project = Merge export type Instance = Merge +export type Disk = Merge export type NetworkInterface = Merge From 125ebef3bb5e2cddb6b1123daac2da55891fbfd3 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 20 Feb 2023 13:06:31 -0600 Subject: [PATCH 16/39] helpers actually save a few lines?!?!?!?!?! --- app/forms/project-edit.tsx | 9 ++++----- .../project/instances/instance/tabs/NetworkingTab.tsx | 10 +++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index 6e1eab8e3..0e2cae6d9 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -17,11 +17,10 @@ import { pb } from 'app/util/path-builder' import { requireProjectParams, useProjectParams, useToast } from '../hooks' EditProjectSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { - const { projectName, orgName } = requireProjectParams(params) - await apiQueryClient.prefetchQuery('projectViewV1', { - path: { project: projectName }, - query: { organization: orgName }, - }) + await apiQueryClient.prefetchQuery( + 'projectViewV1', + toPathQuery('project', toApiSelector(requireProjectParams(params))) + ) return null } diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 8b99a0739..cfe35400e 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -73,9 +73,7 @@ NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { export function NetworkingTab() { const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') - const { orgName, projectName, instanceName } = instanceParams - const projectSelector = { organization: orgName, project: projectName } - const instanceSelector = { instance: instanceName, ...projectSelector } + const instanceSelector = toApiSelector(instanceParams) const queryClient = useApiQueryClient() const addToast = useToast() @@ -102,10 +100,8 @@ export function NetworkingTab() { }) const instanceStopped = - useApiQuery('instanceViewV1', { - path: { instance: instanceName }, - query: projectSelector, - }).data?.runState === 'stopped' + useApiQuery('instanceViewV1', toPathQuery('instance', instanceSelector)).data + ?.runState === 'stopped' const makeActions = (nic: NetworkInterface): MenuAction[] => [ { From 2142354cb5624737eaedd317cba3c7dfed2b96ad Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 20 Feb 2023 16:22:20 -0600 Subject: [PATCH 17/39] nuke all the legacy *ById endpoints in MSW --- app/forms/snapshot-create.tsx | 20 +++--- app/pages/project/disks/DisksPage.tsx | 31 +++++---- .../instances/instance/tabs/NetworkingTab.tsx | 4 +- app/pages/project/snapshots/SnapshotsPage.tsx | 18 ++--- libs/api-mocks/msw/db.ts | 65 ++++++++++------- libs/api-mocks/msw/handlers.ts | 69 +++++++++---------- libs/api/path-params-v1.ts | 3 + 7 files changed, 114 insertions(+), 96 deletions(-) diff --git a/app/forms/snapshot-create.tsx b/app/forms/snapshot-create.tsx index acf4dccd1..3ac0d741b 100644 --- a/app/forms/snapshot-create.tsx +++ b/app/forms/snapshot-create.tsx @@ -1,9 +1,7 @@ import { useNavigate } from 'react-router-dom' import type { PathParams, SnapshotCreate } from '@oxide/api' -import { useApiQuery } from '@oxide/api' -import { useApiMutation } from '@oxide/api' -import { useApiQueryClient } from '@oxide/api' +import { toApiSelector, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' import { @@ -32,17 +30,18 @@ const defaultValues: SnapshotCreate = { export function CreateSnapshotSideModalForm() { const queryClient = useApiQueryClient() - const pathParams = useRequiredParams('orgName', 'projectName') + const projectParams = useRequiredParams('orgName', 'projectName') + const projectSelector = toApiSelector(projectParams) const addToast = useToast() const navigate = useNavigate() - const diskItems = useSnapshotDiskItems(pathParams) + const diskItems = useSnapshotDiskItems(projectParams) - const onDismiss = () => navigate(pb.snapshots(pathParams)) + const onDismiss = () => navigate(pb.snapshots(projectParams)) - const createSnapshot = useApiMutation('snapshotCreate', { + const createSnapshot = useApiMutation('snapshotCreateV1', { onSuccess() { - queryClient.invalidateQueries('snapshotList', { path: pathParams }) + queryClient.invalidateQueries('snapshotListV1', { query: projectSelector }) addToast({ icon: , title: 'Success!', @@ -59,10 +58,7 @@ export function CreateSnapshotSideModalForm() { formOptions={{ defaultValues }} onDismiss={onDismiss} onSubmit={(values) => { - createSnapshot.mutate({ - path: pathParams, - body: values, - }) + createSnapshot.mutate({ query: projectSelector, body: values }) }} submitError={createSnapshot.error} > diff --git a/app/pages/project/disks/DisksPage.tsx b/app/pages/project/disks/DisksPage.tsx index de13541d0..b2e52a702 100644 --- a/app/pages/project/disks/DisksPage.tsx +++ b/app/pages/project/disks/DisksPage.tsx @@ -3,14 +3,15 @@ import { Outlet } from 'react-router-dom' import { Link } from 'react-router-dom' import type { Disk } from '@oxide/api' -import { genName } 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' -import { DateCell } from '@oxide/table' -import { SizeCell } from '@oxide/table' -import { useQueryTable } from '@oxide/table' +import { + apiQueryClient, + genName, + toApiSelector, + useApiMutation, + useApiQuery, + useApiQueryClient, +} from '@oxide/api' +import { DateCell, type MenuAction, SizeCell, useQueryTable } from '@oxide/table' import { EmptyMessage, PageHeader, @@ -39,7 +40,9 @@ function AttachedInstance({ projectName: string instanceId: string }) { - const { data: instance } = useApiQuery('instanceViewById', { path: { id: instanceId } }) + const { data: instance } = useApiQuery('instanceViewV1', { + path: { instance: instanceId }, + }) return instance ? ( { export function DisksPage() { const queryClient = useApiQueryClient() - const { orgName, projectName } = useRequiredParams('orgName', 'projectName') + const projectParams = useRequiredParams('orgName', 'projectName') + const { orgName, projectName } = projectParams + const projectSelector = toApiSelector(projectParams) const { Table, Column } = useQueryTable('diskList', { path: { orgName, projectName } }) const addToast = useToast() @@ -80,9 +85,9 @@ export function DisksPage() { }, }) - const createSnapshot = useApiMutation('snapshotCreate', { + const createSnapshot = useApiMutation('snapshotCreateV1', { onSuccess() { - queryClient.invalidateQueries('snapshotList', { path: { orgName, projectName } }) + queryClient.invalidateQueries('snapshotListV1', { query: projectSelector }) addToast({ icon: , title: 'Success!', @@ -96,7 +101,7 @@ export function DisksPage() { label: 'Snapshot', onActivate() { createSnapshot.mutate({ - path: { orgName, projectName }, + query: projectSelector, body: { name: genName(disk.name), disk: disk.name, diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index cfe35400e..3e56742c5 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -30,7 +30,7 @@ import { pb } from 'app/util/path-builder' const VpcNameFromId = ({ value }: { value: string }) => { const { orgName, projectName } = useRequiredParams('orgName', 'projectName') - const { data: vpc } = useApiQuery('vpcViewById', { path: { id: value } }) + const { data: vpc } = useApiQuery('vpcViewV1', { path: { vpc: value } }) if (!vpc) return null return ( { const SubnetNameFromId = ({ value }: { value: string }) => ( - {useApiQuery('vpcSubnetViewById', { path: { id: value } }).data?.name} + {useApiQuery('vpcSubnetViewV1', { path: { subnet: value } }).data?.name} ) diff --git a/app/pages/project/snapshots/SnapshotsPage.tsx b/app/pages/project/snapshots/SnapshotsPage.tsx index e8b16a32b..1bd0f224b 100644 --- a/app/pages/project/snapshots/SnapshotsPage.tsx +++ b/app/pages/project/snapshots/SnapshotsPage.tsx @@ -2,6 +2,7 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { Link, Outlet } from 'react-router-dom' import type { Snapshot } from '@oxide/api' +import { toApiSelector } from '@oxide/api' import { apiQueryClient, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import type { MenuAction } from '@oxide/table' import { DateCell, SizeCell, useQueryTable } from '@oxide/table' @@ -19,7 +20,7 @@ import { requireProjectParams, useProjectParams, useRequiredParams } from 'app/h import { pb } from 'app/util/path-builder' const DiskNameFromId = ({ value }: { value: string }) => { - const { data: disk } = useApiQuery('diskViewById', { path: { id: value } }) + const { data: disk } = useApiQuery('diskViewV1', { path: { disk: value } }) if (!disk) return null return <>{disk.name} } @@ -35,9 +36,9 @@ const EmptyState = () => ( ) SnapshotsPage.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('snapshotList', { - path: requireProjectParams(params), - query: { limit: 10 }, + const projectSelector = toApiSelector(requireProjectParams(params)) + await apiQueryClient.prefetchQuery('snapshotListV1', { + query: { ...projectSelector, limit: 10 }, }) return null } @@ -45,11 +46,12 @@ SnapshotsPage.loader = async ({ params }: LoaderFunctionArgs) => { export function SnapshotsPage() { const queryClient = useApiQueryClient() const projectParams = useRequiredParams('orgName', 'projectName') - const { Table, Column } = useQueryTable('snapshotList', { path: projectParams }) + const projectSelector = toApiSelector(projectParams) + const { Table, Column } = useQueryTable('snapshotListV1', { query: projectSelector }) - const deleteSnapshot = useApiMutation('snapshotDelete', { + const deleteSnapshot = useApiMutation('snapshotDeleteV1', { onSuccess() { - queryClient.invalidateQueries('snapshotList', { path: projectParams }) + queryClient.invalidateQueries('snapshotListV1', { query: projectSelector }) }, }) @@ -57,7 +59,7 @@ export function SnapshotsPage() { { label: 'Delete', onActivate() { - deleteSnapshot.mutate({ path: { ...projectParams, snapshotName: snapshot.name } }) + deleteSnapshot.mutate({ path: { snapshot: snapshot.name }, query: projectSelector }) }, }, ] diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 03bd6f2d2..23044d483 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -13,16 +13,7 @@ const notFoundBody = { error_code: 'ObjectNotFound' } as const export type NotFound = typeof notFoundBody export const notFoundErr = json({ error_code: 'ObjectNotFound' } as const, { status: 404 }) -export const lookupById = - (table: T[]) => - (params: { path: PP.Id }) => { - const item = table.find((i) => i.id === params.path.id) - if (!item) throw notFoundErr - return item - } - -// TODO: obviously lookupById2 is not the final name -export const lookupById2 = (table: T[], id: string) => { +export const lookupById = (table: T[], id: string) => { const item = table.find((i) => i.id === id) if (!item) throw notFoundErr return item @@ -32,7 +23,7 @@ export const lookup = { org({ organization: id }: PPv1.Org): Json { if (!id) throw notFoundErr - if (isUuid(id)) return lookupById2(db.orgs, id) + if (isUuid(id)) return lookupById(db.orgs, id) const org = db.orgs.find((o) => o.name === id) if (!org) throw notFoundErr @@ -42,7 +33,7 @@ export const lookup = { project({ project: id, ...orgSelector }: PPv1.Project): Json { if (!id) throw notFoundErr - if (isUuid(id)) return lookupById2(db.projects, id) + if (isUuid(id)) return lookupById(db.projects, id) const org = lookup.org(orgSelector) const project = db.projects.find((p) => p.organization_id === org.id && p.name === id) @@ -53,7 +44,7 @@ export const lookup = { instance({ instance: id, ...projectSelector }: PPv1.Instance): Json { if (!id) throw notFoundErr - if (isUuid(id)) return lookupById2(db.instances, id) + if (isUuid(id)) return lookupById(db.instances, id) const project = lookup.project(projectSelector) const instance = db.instances.find((i) => i.project_id === project.id && i.name === id) @@ -67,7 +58,7 @@ export const lookup = { }: PPv1.NetworkInterface): Json { if (!id) throw notFoundErr - if (isUuid(id)) return lookupById2(db.networkInterfaces, id) + if (isUuid(id)) return lookupById(db.networkInterfaces, id) const instance = lookup.instance(instanceSelector) @@ -81,7 +72,7 @@ export const lookup = { disk({ disk: id, ...projectSelector }: PPv1.Disk): Json { if (!id) throw notFoundErr - if (isUuid(id)) return lookupById2(db.disks, id) + if (isUuid(id)) return lookupById(db.disks, id) const project = lookup.project(projectSelector) @@ -90,6 +81,39 @@ export const lookup = { return disk }, + snapshot({ snapshot: id, ...projectSelector }: PPv1.Snapshot): Json { + if (!id) throw notFoundErr + + if (isUuid(id)) return lookupById(db.snapshots, id) + + const project = lookup.project(projectSelector) + const snapshot = db.snapshots.find((i) => i.project_id === project.id && i.name === id) + if (!snapshot) throw notFoundErr + + return snapshot + }, + vpc({ vpc: id, ...projectSelector }: PPv1.Vpc): Json { + if (!id) throw notFoundErr + + if (isUuid(id)) return lookupById(db.vpcs, id) + + const project = lookup.project(projectSelector) + const vpc = db.vpcs.find((v) => v.project_id === project.id && v.name === id) + if (!vpc) throw notFoundErr + + return vpc + }, + vpcSubnet({ subnet: id, ...vpcSelector }: PPv1.VpcSubnet): Json { + if (!id) throw notFoundErr + + if (isUuid(id)) return lookupById(db.vpcSubnets, id) + + const vpc = lookup.vpc(vpcSelector) + const subnet = db.vpcSubnets.find((s) => s.vpc_id === vpc.id && s.name === id) + if (!subnet) throw notFoundErr + + return subnet + }, } export function lookupOrg(params: PP.Org): Json { @@ -151,17 +175,6 @@ export function lookupImage(params: PP.Image): Json { return image } -export function lookupSnapshot(params: PP.Snapshot): Json { - const project = lookupProject(params) - - const snapshot = db.snapshots.find( - (s) => s.project_id === project.id && s.name === params.snapshotName - ) - if (!snapshot) throw notFoundErr - - return snapshot -} - export function lookupVpcSubnet(params: PP.VpcSubnet): Json { const vpc = lookupVpc(params) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 1846cba47..ced158aa5 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -24,7 +24,6 @@ import { lookupSamlIdp, lookupSilo, lookupSled, - lookupSnapshot, lookupSshKey, lookupSystemUpdate, lookupVpc, @@ -179,7 +178,7 @@ export const handlers = makeHandlers({ return json(newDisk, { status: 201 }) }, - diskView: (params) => lookupDisk(params.path), + diskViewV1: ({ path, query }) => lookup.disk({ ...path, ...query }), diskDelete(params) { const disk = lookupDisk(params.path) @@ -517,17 +516,17 @@ export const handlers = makeHandlers({ // TODO: Is this the right thing to return? return body }, - snapshotList(params) { - const project = lookupProject(params.path) + snapshotListV1(params) { + const project = lookup.project(params.query) const snapshots = db.snapshots.filter((i) => i.project_id === project.id) return paginated(params.query, snapshots) }, - snapshotCreate({ body, ...params }) { - const project = lookupProject(params.path) + snapshotCreateV1({ body, query }) { + const project = lookup.project(query) errIfExists(db.snapshots, { name: body.name }) - const disk = lookupDisk({ ...params.path, diskName: body.disk }) + const disk = lookup.disk({ ...query, disk: body.disk }) const newSnapshot: Json = { id: uuid(), @@ -542,9 +541,9 @@ export const handlers = makeHandlers({ return json(newSnapshot, { status: 201 }) }, - snapshotView: (params) => lookupSnapshot(params.path), - snapshotDelete(params) { - const snapshot = lookupSnapshot(params.path) + snapshotViewV1: ({ path, query }) => lookup.snapshot({ ...path, ...query }), + snapshotDeleteV1({ path, query }) { + const snapshot = lookup.snapshot({ ...path, ...query }) db.snapshots = db.snapshots.filter((s) => s.id !== snapshot.id) return 204 }, @@ -583,6 +582,7 @@ export const handlers = makeHandlers({ return json(newVpc, { status: 201 }) }, vpcView: (params) => lookupVpc(params.path), + vpcViewV1: ({ path, query }) => lookup.vpc({ ...path, ...query }), vpcUpdate({ body, ...params }) { const vpc = lookupVpc(params.path) @@ -744,7 +744,7 @@ export const handlers = makeHandlers({ db.vpcSubnets.push(newSubnet) return json(newSubnet, { status: 201 }) }, - vpcSubnetView: (params) => lookupVpcSubnet(params.path), + vpcSubnetViewV1: ({ path, query }) => lookup.vpcSubnet({ ...path, ...query }), vpcSubnetUpdate({ body, ...params }) { const subnet = lookupVpcSubnet(params.path) @@ -972,7 +972,7 @@ export const handlers = makeHandlers({ } }, updateDeploymentsList: (params) => paginated(params.query, db.updateDeployments), - updateDeploymentView: lookupById(db.updateDeployments), + updateDeploymentView: ({ path: { id } }) => lookupById(db.updateDeployments, id), systemMetric: (params) => { // const result = ZVal.ResourceName.safeParse(req.params.resourceName) @@ -997,22 +997,6 @@ export const handlers = makeHandlers({ } }, - // by ID endpoints (will be gone soon) - - diskViewById: lookupById(db.disks), - imageViewById: lookupById(db.images), - instanceNetworkInterfaceViewById: lookupById(db.networkInterfaces), - instanceViewById: lookupById(db.instances), - organizationViewById: lookupById(db.orgs), - projectViewById: lookupById(db.projects), - siloViewById: lookupById(db.silos), - snapshotViewById: lookupById(db.snapshots), - systemImageViewById: lookupById(db.globalImages), - vpcRouterRouteViewById: lookupById(db.vpcRouterRoutes), - vpcRouterViewById: lookupById(db.vpcRouters), - vpcSubnetViewById: lookupById(db.vpcSubnets), - vpcViewById: lookupById(db.vpcs), - // Misc endpoints we're not using yet in the console certificateCreate: NotImplemented, @@ -1066,7 +1050,6 @@ export const handlers = makeHandlers({ diskCreateV1: NotImplemented, diskDeleteV1: NotImplemented, diskListV1: NotImplemented, - diskViewV1: NotImplemented, instanceCreateV1: NotImplemented, instanceDeleteV1: NotImplemented, instanceListV1: NotImplemented, @@ -1094,10 +1077,6 @@ export const handlers = makeHandlers({ sledListV1: NotImplemented, sledPhysicalDiskListV1: NotImplemented, sledViewV1: NotImplemented, - snapshotCreateV1: NotImplemented, - snapshotDeleteV1: NotImplemented, - snapshotListV1: NotImplemented, - snapshotViewV1: NotImplemented, systemPolicyUpdateV1: NotImplemented, systemPolicyViewV1: NotImplemented, vpcCreateV1: NotImplemented, @@ -1117,12 +1096,27 @@ export const handlers = makeHandlers({ vpcSubnetDeleteV1: NotImplemented, vpcSubnetListV1: NotImplemented, vpcSubnetUpdateV1: NotImplemented, - vpcSubnetViewV1: NotImplemented, vpcUpdateV1: NotImplemented, - vpcViewV1: NotImplemented, + + // deprecated by ID endpoints + + diskViewById: NotImplemented, + imageViewById: NotImplemented, + instanceNetworkInterfaceViewById: NotImplemented, + instanceViewById: NotImplemented, + organizationViewById: NotImplemented, + projectViewById: NotImplemented, + siloViewById: NotImplemented, + snapshotViewById: NotImplemented, + systemImageViewById: NotImplemented, + vpcRouterRouteViewById: NotImplemented, + vpcRouterViewById: NotImplemented, + vpcSubnetViewById: NotImplemented, + vpcViewById: NotImplemented, // Deprecated endpoints + diskView: NotImplemented, instanceDiskAttach: NotImplemented, instanceDiskDetach: NotImplemented, instanceDiskList: NotImplemented, @@ -1138,4 +1132,9 @@ export const handlers = makeHandlers({ projectList: NotImplemented, projectUpdate: NotImplemented, projectView: NotImplemented, + snapshotCreate: NotImplemented, + snapshotDelete: NotImplemented, + snapshotList: NotImplemented, + snapshotView: NotImplemented, + vpcSubnetView: NotImplemented, }) diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index 06494d86c..6871feebe 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -7,3 +7,6 @@ export type Project = Merge export type Instance = Merge export type Disk = Merge export type NetworkInterface = Merge +export type Snapshot = Merge +export type Vpc = Merge +export type VpcSubnet = Merge From 02203a3574e76e29ac93ba5199b61fc7d327eb0e Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 20 Feb 2023 17:14:06 -0600 Subject: [PATCH 18/39] disk endpoints --- app/forms/disk-attach.tsx | 32 ++++++------- app/forms/disk-create.tsx | 9 ++-- app/forms/snapshot-create.tsx | 10 +++-- app/pages/project/disks/DisksPage.tsx | 28 ++++-------- libs/api-mocks/msw/db.ts | 22 +++------ libs/api-mocks/msw/handlers.ts | 65 +++++++++++++-------------- libs/api-mocks/msw/util.ts | 7 +-- libs/api/path-params-v1.ts | 3 ++ libs/api/selector.ts | 26 ----------- 9 files changed, 74 insertions(+), 128 deletions(-) diff --git a/app/forms/disk-attach.tsx b/app/forms/disk-attach.tsx index d59642b1e..375220313 100644 --- a/app/forms/disk-attach.tsx +++ b/app/forms/disk-attach.tsx @@ -1,16 +1,11 @@ +import { useParams } from 'react-router-dom' import invariant from 'tiny-invariant' import type { Disk, DiskIdentifier } from '@oxide/api' -import { - toApiSelector, - toPathQuery, - useApiMutation, - useApiQuery, - useApiQueryClient, -} from '@oxide/api' +import { toApiSelector, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import { ListboxField, SideModalForm } from 'app/components/form' -import { useAllParams } from 'app/hooks' +import { useProjectParams } from 'app/hooks' const defaultValues = { name: '' } @@ -27,15 +22,18 @@ export function AttachDiskSideModalForm({ onDismiss, }: AttachDiskProps) { const queryClient = useApiQueryClient() - const { orgName, projectName, instanceName } = useAllParams('orgName', 'projectName') + // instance name undefined when this form is called from DisksTableField on + // instance create, which passes in its own onSubmit, bypassing the attachDisk mutation + const { instanceName } = useParams() + const projectSelector = toApiSelector(useProjectParams()) const attachDisk = useApiMutation('instanceDiskAttachV1', { onSuccess(data) { invariant(instanceName, 'instanceName is required') - queryClient.invalidateQueries( - 'instanceDiskListV1', - toPathQuery('instance', toApiSelector({ orgName, projectName, instanceName })) - ) + queryClient.invalidateQueries('instanceDiskListV1', { + path: { instance: instanceName }, + query: projectSelector, + }) onSuccess?.(data) onDismiss() }, @@ -46,7 +44,7 @@ export function AttachDiskSideModalForm({ // click in // TODO: error handling const detachedDisks = - useApiQuery('diskList', { path: { orgName, projectName } }).data?.items.filter( + useApiQuery('diskListV1', { query: projectSelector }).data?.items.filter( (d) => d.state.state === 'detached' ) || [] @@ -60,10 +58,8 @@ export function AttachDiskSideModalForm({ (({ name }) => { invariant(instanceName, 'instanceName is required') attachDisk.mutate({ - ...toPathQuery( - 'instance', - toApiSelector({ orgName, projectName, instanceName }) - ), + path: { instance: instanceName }, + query: projectSelector, body: { disk: name }, }) }) diff --git a/app/forms/disk-create.tsx b/app/forms/disk-create.tsx index 3cacc7079..9d3dce7d0 100644 --- a/app/forms/disk-create.tsx +++ b/app/forms/disk-create.tsx @@ -2,6 +2,7 @@ import type { NavigateFunction } from 'react-router-dom' import { useNavigate } from 'react-router-dom' import type { BlockSize, Disk, DiskCreate } from '@oxide/api' +import { toApiSelector } from '@oxide/api' import { useApiMutation, useApiQueryClient } from '@oxide/api' import { Divider, Success16Icon } from '@oxide/ui' import { GiB } from '@oxide/util' @@ -46,13 +47,13 @@ export function CreateDiskSideModalForm({ onDismiss, }: CreateSideModalFormProps) { const queryClient = useApiQueryClient() - const pathParams = useRequiredParams('orgName', 'projectName') + const projectSelector = toApiSelector(useRequiredParams('orgName', 'projectName')) const addToast = useToast() const navigate = useNavigate() - const createDisk = useApiMutation('diskCreate', { + const createDisk = useApiMutation('diskCreateV1', { onSuccess(data) { - queryClient.invalidateQueries('diskList', { path: pathParams }) + queryClient.invalidateQueries('diskListV1', { query: projectSelector }) addToast({ icon: , title: 'Success!', @@ -71,7 +72,7 @@ export function CreateDiskSideModalForm({ onDismiss={() => onDismiss(navigate)} onSubmit={({ size, ...rest }) => { const body = { size: size * GiB, ...rest } - onSubmit ? onSubmit(body) : createDisk.mutate({ path: pathParams, body }) + onSubmit ? onSubmit(body) : createDisk.mutate({ query: projectSelector, body }) }} loading={createDisk.isLoading} submitError={createDisk.error} diff --git a/app/forms/snapshot-create.tsx b/app/forms/snapshot-create.tsx index 3ac0d741b..1c90ff04b 100644 --- a/app/forms/snapshot-create.tsx +++ b/app/forms/snapshot-create.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom' -import type { PathParams, SnapshotCreate } from '@oxide/api' +import type { PathParamsV1, SnapshotCreate } from '@oxide/api' import { toApiSelector, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' @@ -13,8 +13,10 @@ import { import { useRequiredParams, useToast } from 'app/hooks' import { pb } from 'app/util/path-builder' -const useSnapshotDiskItems = (params: PathParams.Project) => { - const { data: disks } = useApiQuery('diskList', { path: params, query: { limit: 1000 } }) +const useSnapshotDiskItems = (projectSelector: PathParamsV1.Project) => { + const { data: disks } = useApiQuery('diskListV1', { + query: { ...projectSelector, limit: 1000 }, + }) return ( disks?.items .filter((disk) => disk.state.state === 'attached') @@ -35,7 +37,7 @@ export function CreateSnapshotSideModalForm() { const addToast = useToast() const navigate = useNavigate() - const diskItems = useSnapshotDiskItems(projectParams) + const diskItems = useSnapshotDiskItems(projectSelector) const onDismiss = () => navigate(pb.snapshots(projectParams)) diff --git a/app/pages/project/disks/DisksPage.tsx b/app/pages/project/disks/DisksPage.tsx index b2e52a702..16fed77b1 100644 --- a/app/pages/project/disks/DisksPage.tsx +++ b/app/pages/project/disks/DisksPage.tsx @@ -64,9 +64,9 @@ const EmptyState = () => ( ) DisksPage.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('diskList', { - path: requireProjectParams(params), - query: { limit: 10 }, + const projectSelector = toApiSelector(requireProjectParams(params)) + await apiQueryClient.prefetchQuery('diskListV1', { + query: { ...projectSelector, limit: 10 }, }) return null } @@ -74,14 +74,13 @@ DisksPage.loader = async ({ params }: LoaderFunctionArgs) => { export function DisksPage() { const queryClient = useApiQueryClient() const projectParams = useRequiredParams('orgName', 'projectName') - const { orgName, projectName } = projectParams const projectSelector = toApiSelector(projectParams) - const { Table, Column } = useQueryTable('diskList', { path: { orgName, projectName } }) + const { Table, Column } = useQueryTable('diskListV1', { query: projectSelector }) const addToast = useToast() - const deleteDisk = useApiMutation('diskDelete', { + const deleteDisk = useApiMutation('diskDeleteV1', { onSuccess() { - queryClient.invalidateQueries('diskList', { path: { orgName, projectName } }) + queryClient.invalidateQueries('diskListV1', { query: projectSelector }) }, }) @@ -116,7 +115,7 @@ export function DisksPage() { { label: 'Delete', onActivate: () => { - deleteDisk.mutate({ path: { orgName, projectName, diskName: disk.name } }) + deleteDisk.mutate({ path: { disk: disk.name }, query: projectSelector }) }, disabled: !['detached', 'creating', 'faulted'].includes(disk.state.state) && @@ -132,10 +131,7 @@ export function DisksPage() { }>Disks - + New Disk @@ -151,13 +147,7 @@ export function DisksPage() { 'instance' in disk.state ? disk.state.instance : null } cell={({ value }: { value: string | undefined }) => - value ? ( - - ) : null + value ? : null } /> diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 23044d483..0ba597c7a 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -114,6 +114,11 @@ export const lookup = { return subnet }, + systemUpdate({ version }: PPv1.SystemUpdate): Json { + const update = db.systemUpdates.find((o) => o.version === version) + if (!update) throw notFoundErr + return update + }, } export function lookupOrg(params: PP.Org): Json { @@ -153,17 +158,6 @@ export function lookupInstance(params: PP.Instance): Json { return instance } -export function lookupDisk(params: PP.Disk): Json { - const project = lookupProject(params) - - const disk = db.disks.find( - (d) => d.project_id === project.id && d.name === params.diskName - ) - if (!disk) throw notFoundErr - - return disk -} - export function lookupImage(params: PP.Image): Json { const project = lookupProject(params) @@ -247,12 +241,6 @@ export function lookupSled(params: PP.Id): Json { return sled } -export function lookupSystemUpdate(params: PP.SystemUpdate): Json { - const update = db.systemUpdates.find((o) => o.version === params.version) - if (!update) throw notFoundErr - return update -} - const initDb = { disks: [...mock.disks], globalImages: [...mock.globalImages], diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index ced158aa5..2b550a1e4 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -15,7 +15,6 @@ import { db, lookup, lookupById, - lookupDisk, lookupGlobalImage, lookupImage, lookupInstance, @@ -25,7 +24,6 @@ import { lookupSilo, lookupSled, lookupSshKey, - lookupSystemUpdate, lookupVpc, lookupVpcRouter, lookupVpcRouterRoute, @@ -149,14 +147,14 @@ export const handlers = makeHandlers({ return 204 }, - diskList(params) { - const project = lookupProject(params.path) + diskListV1({ query }) { + const project = lookup.project(query) const disks = db.disks.filter((d) => d.project_id === project.id) - return paginated(params.query, disks) + return paginated(query, disks) }, - diskCreate({ body, ...params }) { - const project = lookupProject(params.path) + diskCreateV1({ body, query }) { + const project = lookup.project(query) errIfExists(db.disks, { name: body.name, project_id: project.id }) @@ -179,8 +177,8 @@ export const handlers = makeHandlers({ return json(newDisk, { status: 201 }) }, diskViewV1: ({ path, query }) => lookup.disk({ ...path, ...query }), - diskDelete(params) { - const disk = lookupDisk(params.path) + diskDeleteV1({ path, query }) { + const disk = lookup.disk({ ...path, ...query }) // Governed by https://github.com/oxidecomputer/omicron/blob/e5704d7f343fa0633751527dedf276409647ad4e/nexus/src/db/datastore.rs#L2103 switch (disk.state.state) { @@ -194,10 +192,14 @@ export const handlers = makeHandlers({ db.disks = db.disks.filter((d) => d.id !== disk.id) return 204 }, - diskMetricsList(params) { - lookupDisk(params.path) + diskMetricsList({ path, query }) { + lookup.disk({ + organization: path.orgName, + project: path.projectName, + disk: path.diskName, + }) - const { startTime, endTime } = getStartAndEndTime(params.query) + const { startTime, endTime } = getStartAndEndTime(query) if (endTime <= startTime) return { items: [] } @@ -241,8 +243,8 @@ export const handlers = makeHandlers({ const instances = db.instances.filter((i) => i.project_id === project.id) return paginated(params.query, instances) }, - instanceCreate({ body, ...params }) { - const project = lookupProject(params.path) + instanceCreateV1({ body, query }) { + const project = lookup.project(query) errIfExists(db.instances, { name: body.name, project_id: project.id }) @@ -255,9 +257,9 @@ export const handlers = makeHandlers({ for (const diskParams of body.disks || []) { if (diskParams.type === 'create') { errIfExists(db.disks, { name: diskParams.name, project_id: project.id }) - errIfInvalidDiskSize(params.path, diskParams) + errIfInvalidDiskSize(diskParams) } else { - lookupDisk({ ...params.path, diskName: diskParams.name }) + lookup.disk({ ...query, disk: diskParams.name }) } } @@ -267,12 +269,8 @@ export const handlers = makeHandlers({ */ if (body.network_interfaces?.type === 'create') { body.network_interfaces.params.forEach(({ vpc_name, subnet_name }) => { - lookupVpc({ ...params.path, vpcName: vpc_name }) - lookupVpcSubnet({ - ...params.path, - vpcName: vpc_name, - subnetName: subnet_name, - }) + lookup.vpc({ ...query, vpc: vpc_name }) + lookup.vpcSubnet({ ...query, vpc: vpc_name, subnet: subnet_name }) }) } @@ -292,7 +290,7 @@ export const handlers = makeHandlers({ } db.disks.push(newDisk) } else { - const disk = lookupDisk({ ...params.path, diskName: diskParams.name }) + const disk = lookup.disk({ ...query, disk: diskParams.name }) disk.state = { state: 'attached', instance: instanceId } } } @@ -321,12 +319,9 @@ export const handlers = makeHandlers({ primary: i === 0 ? true : false, mac: '00:00:00:00:00:00', ip: ip || '127.0.0.1', - vpc_id: lookupVpc({ ...params.path, vpcName: vpc_name }).id, - subnet_id: lookupVpcSubnet({ - ...params.path, - vpcName: vpc_name, - subnetName: subnet_name, - }).id, + vpc_id: lookup.vpc({ ...query, vpc: vpc_name }).id, + subnet_id: lookup.vpcSubnet({ ...query, vpc: vpc_name, subnet: subnet_name }) + .id, ...getTimestamps(), }) } @@ -925,9 +920,9 @@ export const handlers = makeHandlers({ }, systemUpdateList: (params) => paginated(params.query, db.systemUpdates), - systemUpdateView: ({ path }) => lookupSystemUpdate(path), + systemUpdateView: ({ path }) => lookup.systemUpdate(path), systemUpdateComponentsList: (params) => { - const systemUpdate = lookupSystemUpdate(params.path) + const systemUpdate = lookup.systemUpdate(params.path) const ids = new Set( db.systemUpdateComponentUpdates .filter((o) => o.system_update_id === systemUpdate.id) @@ -1047,10 +1042,6 @@ export const handlers = makeHandlers({ certificateDeleteV1: NotImplemented, certificateListV1: NotImplemented, certificateViewV1: NotImplemented, - diskCreateV1: NotImplemented, - diskDeleteV1: NotImplemented, - diskListV1: NotImplemented, - instanceCreateV1: NotImplemented, instanceDeleteV1: NotImplemented, instanceListV1: NotImplemented, instanceMigrateV1: NotImplemented, @@ -1116,7 +1107,11 @@ export const handlers = makeHandlers({ // Deprecated endpoints + diskCreate: NotImplemented, + diskDelete: NotImplemented, + diskList: NotImplemented, diskView: NotImplemented, + instanceCreate: NotImplemented, instanceDiskAttach: NotImplemented, instanceDiskDetach: NotImplemented, instanceDiskList: NotImplemented, diff --git a/libs/api-mocks/msw/util.ts b/libs/api-mocks/msw/util.ts index 68424b738..b3a302ea4 100644 --- a/libs/api-mocks/msw/util.ts +++ b/libs/api-mocks/msw/util.ts @@ -1,6 +1,6 @@ import { subHours } from 'date-fns' -import type { DiskCreate, DiskCreatePathParams } from '@oxide/api' +import type { DiskCreate } from '@oxide/api' import type { Json } from '@oxide/gen/msw-handlers' import { json } from '@oxide/gen/msw-handlers' import { GiB } from '@oxide/util' @@ -92,10 +92,7 @@ export const errIfExists = >( } } -export const errIfInvalidDiskSize = ( - params: DiskCreatePathParams, - disk: Json -) => { +export const errIfInvalidDiskSize = (disk: Json) => { const source = disk.disk_source if (source.type === 'snapshot') { const snapshotSize = db.snapshots.find((s) => source.snapshot_id === s.id)?.size ?? 0 diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index 6871feebe..e900ef5d8 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -10,3 +10,6 @@ export type NetworkInterface = Merge export type Snapshot = Merge export type Vpc = Merge export type VpcSubnet = Merge +export type SystemUpdate = { version: string } + +export type Id = { id: string } diff --git a/libs/api/selector.ts b/libs/api/selector.ts index f158507db..e218dbd51 100644 --- a/libs/api/selector.ts +++ b/libs/api/selector.ts @@ -2,32 +2,6 @@ import type { Replace } from 'type-fest' import { exclude } from '@oxide/util' -// notes on needed helpers: sometimes you need to select an instance with -// -// { path: { instance }, query: { project, organization } } -// -// and sometimes you need -// -// { query: { instance, project, organization } } -// -// so converting between those two forms probably makes sense. Having a name for -// each form will probably be necessary. Another thing we often need to do is -// extract a selector from the RR path params -// -// PathBuilder probably also needs to change to take the selector form instead -// of the names -// -// We may also want to change the names of the params in the URL? on the other -// hand it's nice to be explcit that they're names if that's what they are, and -// the helpers will make it tolerable. If we plan on keeping the route structure -// in the console, then maybe we actually want the helpers to go the other way: -// the canonical form is `{ orgName, projectName, instanceName }`, and we have -// helpers `toQuery` and `toPathQuery` for converting to the API forms. In order -// to do this cleanly, we should probably make it so the names can be converted -// trivially by stripping `-Name` off the end. Does this mean we need to change -// `orgName` to `organizationName`? That would be huge headache so it might be -// better to handle that one specially for now. However, that messes up the types. - /** * Convert a selector to API params with one key in `path` and the rest in * `query`. To add extra query params like `limit`, just include them in From efce7cf8bfe2e38d37e264d3b771465915d97c99 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 20 Feb 2023 17:21:59 -0600 Subject: [PATCH 19/39] org create and fix instance create --- app/forms/instance-create.tsx | 24 ++++++++++-------------- app/forms/org-create.tsx | 5 ++--- libs/api-mocks/msw/handlers.ts | 4 ++-- libs/api/__tests__/hooks.spec.tsx | 9 +++++---- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 0cb920c56..944337517 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -3,6 +3,7 @@ import invariant from 'tiny-invariant' import type { SetRequired } from 'type-fest' import type { InstanceCreate } from '@oxide/api' +import { toApiSelector } from '@oxide/api' import { apiQueryClient } from '@oxide/api' import { genName } from '@oxide/api' import { useApiQuery } from '@oxide/api' @@ -76,23 +77,18 @@ CreateInstanceForm.loader = async () => { export function CreateInstanceForm() { const queryClient = useApiQueryClient() const addToast = useToast() - const pageParams = useRequiredParams('orgName', 'projectName') + const projectParams = useRequiredParams('orgName', 'projectName') + const projectSelector = toApiSelector(projectParams) const navigate = useNavigate() - const createInstance = useApiMutation('instanceCreate', { + const createInstance = useApiMutation('instanceCreateV1', { onSuccess(instance) { // refetch list of instances - queryClient.invalidateQueries('instanceList', { path: pageParams }) + queryClient.invalidateQueries('instanceList', { path: projectParams }) // avoid the instance fetch when the instance page loads since we have the data queryClient.setQueryData( 'instanceViewV1', - { - path: { instance: instance.name }, - query: { - project: pageParams.projectName, - organization: pageParams.orgName, - }, - }, + { path: { instance: instance.name }, query: projectSelector }, instance ) addToast({ @@ -100,7 +96,7 @@ export function CreateInstanceForm() { title: 'Success!', content: 'Your instance has been created.', }) - navigate(pb.instancePage({ ...pageParams, instanceName: instance.name })) + navigate(pb.instancePage({ ...projectParams, instanceName: instance.name })) }, }) @@ -134,11 +130,11 @@ export function CreateInstanceForm() { const bootDiskName = values.bootDiskName || genName(values.name, image.name) createInstance.mutate({ - path: pageParams, + query: projectSelector, body: { name: values.name, hostname: values.hostname || values.name, - description: `An instance in project: ${pageParams.projectName}`, + description: `An instance in project: ${projectParams.projectName}`, memory: instance.memory * GiB, ncpus: instance.ncpus, disks: [ @@ -269,7 +265,7 @@ export function CreateInstanceForm() { Create instance - navigate(pb.instances(pageParams))} /> + navigate(pb.instances(projectParams))} /> )} diff --git a/app/forms/org-create.tsx b/app/forms/org-create.tsx index 7859bf82f..f6968bc0d 100644 --- a/app/forms/org-create.tsx +++ b/app/forms/org-create.tsx @@ -18,11 +18,10 @@ export function CreateOrgSideModalForm() { const queryClient = useApiQueryClient() const addToast = useToast() - const createOrg = useApiMutation('organizationCreate', { + const createOrg = useApiMutation('organizationCreateV1', { onSuccess(org) { queryClient.invalidateQueries('organizationListV1', {}) // avoid the org fetch when the org page loads since we have the data - const orgParams = { orgName: org.name } queryClient.setQueryData( 'organizationViewV1', { path: { organization: org.name } }, @@ -33,7 +32,7 @@ export function CreateOrgSideModalForm() { title: 'Success!', content: 'Your organization has been created.', }) - navigate(pb.projects(orgParams)) + navigate(pb.projects({ orgName: org.name })) }, }) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 2b550a1e4..4cf53e7cd 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -51,7 +51,7 @@ export const handlers = makeHandlers({ groupList: (params) => paginated(params.query, db.userGroups), organizationListV1: (params) => paginated(params.query, db.orgs), - organizationCreate({ body }) { + organizationCreateV1({ body }) { errIfExists(db.orgs, { name: body.name }) const newOrg: Json = { @@ -1051,7 +1051,6 @@ export const handlers = makeHandlers({ instanceSerialConsoleV1: NotImplemented, instanceStartV1: NotImplemented, instanceStopV1: NotImplemented, - organizationCreateV1: NotImplemented, organizationDeleteV1: NotImplemented, organizationPolicyUpdateV1: NotImplemented, organizationPolicyViewV1: NotImplemented, @@ -1120,6 +1119,7 @@ export const handlers = makeHandlers({ instanceNetworkInterfaceUpdate: NotImplemented, instanceNetworkInterfaceView: NotImplemented, instanceView: NotImplemented, + organizationCreate: NotImplemented, organizationList: NotImplemented, organizationView: NotImplemented, projectCreate: NotImplemented, diff --git a/libs/api/__tests__/hooks.spec.tsx b/libs/api/__tests__/hooks.spec.tsx index 2bf2baa8a..c8a5fe587 100644 --- a/libs/api/__tests__/hooks.spec.tsx +++ b/libs/api/__tests__/hooks.spec.tsx @@ -35,7 +35,8 @@ const renderGetOrg503 = () => config ) -const renderCreateOrg = () => renderHook(() => useApiMutation('organizationCreate'), config) +const renderCreateOrg = () => + renderHook(() => useApiMutation('organizationCreateV1'), config) const createParams = { body: { name: 'abc', description: '', hello: 'a' }, @@ -206,7 +207,7 @@ describe('useApiMutation', () => { }) it('contains client_error if error body is not json', async () => { - overrideOnce('post', '/api/organizations', 404, 'not json') + overrideOnce('post', '/api/v1/organizations', 404, 'not json') const { result } = renderCreateOrg() act(() => result.current.mutate(createParams)) @@ -223,7 +224,7 @@ describe('useApiMutation', () => { }) it('does not client_error if response body is empty', async () => { - overrideOnce('post', '/api/organizations', 503, '') + overrideOnce('post', '/api/v1/organizations', 503, '') const { result } = renderCreateOrg() act(() => result.current.mutate(createParams)) @@ -255,7 +256,7 @@ describe('useApiMutation', () => { // RQ doesn't like a value of undefined for data, so we're using {} for now it('returns success with empty object if response body is empty', async () => { - overrideOnce('post', '/api/organizations', 204, '') + overrideOnce('post', '/api/v1/organizations', 204, '') const { result } = renderCreateOrg() act(() => result.current.mutate(createParams)) From 9f9918c2e8e6376fe59a514498ec5d77bdb6edfb Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 10:12:02 -0600 Subject: [PATCH 20/39] go around eliminating use of toApiSelector in the wild --- app/forms/disk-create.tsx | 5 +- app/forms/project-create.tsx | 20 ++-- app/forms/project-edit.tsx | 35 +++---- app/forms/snapshot-create.tsx | 11 +-- app/hooks/use-params.ts | 5 + app/pages/project/disks/DisksPage.tsx | 31 +++---- .../instances/instance/InstancePage.tsx | 49 +++++----- .../instances/instance/tabs/MetricsTab.tsx | 16 ++-- .../instances/instance/tabs/NetworkingTab.tsx | 13 ++- .../instances/instance/tabs/StorageTab.tsx | 27 +++--- app/pages/project/snapshots/SnapshotsPage.tsx | 15 ++- app/util/path-builder.ts | 93 ++++++++++++++++++- libs/api/path-params-v1.ts | 1 + 13 files changed, 193 insertions(+), 128 deletions(-) diff --git a/app/forms/disk-create.tsx b/app/forms/disk-create.tsx index 9d3dce7d0..0266481db 100644 --- a/app/forms/disk-create.tsx +++ b/app/forms/disk-create.tsx @@ -2,7 +2,6 @@ import type { NavigateFunction } from 'react-router-dom' import { useNavigate } from 'react-router-dom' import type { BlockSize, Disk, DiskCreate } from '@oxide/api' -import { toApiSelector } from '@oxide/api' import { useApiMutation, useApiQueryClient } from '@oxide/api' import { Divider, Success16Icon } from '@oxide/ui' import { GiB } from '@oxide/util' @@ -14,7 +13,7 @@ import { RadioField, SideModalForm, } from 'app/components/form' -import { useRequiredParams, useToast } from 'app/hooks' +import { useProjectSelector, useToast } from 'app/hooks' const defaultValues: DiskCreate = { name: '', @@ -47,7 +46,7 @@ export function CreateDiskSideModalForm({ onDismiss, }: CreateSideModalFormProps) { const queryClient = useApiQueryClient() - const projectSelector = toApiSelector(useRequiredParams('orgName', 'projectName')) + const projectSelector = useProjectSelector() const addToast = useToast() const navigate = useNavigate() diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index 732c79227..3004dbf55 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -1,13 +1,13 @@ import { useNavigate } from 'react-router-dom' import type { ProjectCreate } from '@oxide/api' -import { toApiSelector, toPathQuery, useApiMutation, useApiQueryClient } from '@oxide/api' +import { toPathQuery, useApiMutation, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' -import { useRequiredParams, useToast } from '../hooks' +import { useOrgSelector, useToast } from '../hooks' const defaultValues: ProjectCreate = { name: '', @@ -19,19 +19,19 @@ export function CreateProjectSideModalForm() { const queryClient = useApiQueryClient() const addToast = useToast() - const { orgName } = useRequiredParams('orgName') + const { organization } = useOrgSelector() - const onDismiss = () => navigate(pb.projects({ orgName })) + const onDismiss = () => navigate(pb2.projects({ organization })) const createProject = useApiMutation('projectCreateV1', { onSuccess(project) { // refetch list of projects in sidebar - queryClient.invalidateQueries('projectListV1', { query: { organization: orgName } }) + queryClient.invalidateQueries('projectListV1', { query: { organization } }) // avoid the project fetch when the project page loads since we have the data - const projectParams = { orgName, projectName: project.name } + const projectSelector = { organization, project: project.name } queryClient.setQueryData( 'projectViewV1', - toPathQuery('project', toApiSelector(projectParams)), + toPathQuery('project', projectSelector), project ) addToast({ @@ -39,7 +39,7 @@ export function CreateProjectSideModalForm() { title: 'Success!', content: 'Your project has been created.', }) - navigate(pb.instances(projectParams)) + navigate(pb2.instances(projectSelector)) }, }) @@ -51,7 +51,7 @@ export function CreateProjectSideModalForm() { onDismiss={onDismiss} onSubmit={({ name, description }) => { createProject.mutate({ - query: { organization: orgName }, + query: { organization }, body: { name, description }, }) }} diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index 0e2cae6d9..b94acf4af 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom' import { apiQueryClient, - toApiSelector, toPathQuery, useApiMutation, useApiQuery, @@ -12,14 +11,14 @@ import { import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' -import { requireProjectParams, useProjectParams, useToast } from '../hooks' +import { getProjectSelector, useProjectSelector, useToast } from '../hooks' EditProjectSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { await apiQueryClient.prefetchQuery( 'projectViewV1', - toPathQuery('project', toApiSelector(requireProjectParams(params))) + toPathQuery('project', getProjectSelector(params)) ) return null } @@ -29,32 +28,23 @@ export function EditProjectSideModalForm() { const addToast = useToast() const navigate = useNavigate() - const projectParams = useProjectParams() - const { orgName } = projectParams + const projectSelector = useProjectSelector() + const projectPathQuery = toPathQuery('project', projectSelector) + const { organization } = projectSelector - const onDismiss = () => navigate(pb.projects({ orgName })) + const onDismiss = () => navigate(pb2.projects(projectSelector)) - const { data: project } = useApiQuery( - 'projectViewV1', - // ok, I immediately feel this is a bad idea and want to change course. too - // many function calls. type inference on hover helps show what you're doing - // but it's still very alienating from the simple objects actually being - // passed around - toPathQuery('project', toApiSelector(projectParams)) - ) + const { data: project } = useApiQuery('projectViewV1', projectPathQuery) const editProject = useApiMutation('projectUpdateV1', { onSuccess(project) { // refetch list of projects in sidebar // TODO: check this invalidation - queryClient.invalidateQueries('projectListV1', { query: { organization: orgName } }) + queryClient.invalidateQueries('projectListV1', { query: { organization } }) // avoid the project fetch when the project page loads since we have the data queryClient.setQueryData( 'projectViewV1', - { - path: { project: project.name }, - query: { organization: orgName }, - }, + { path: { project: project.name }, query: { organization } }, project ) addToast({ @@ -73,10 +63,7 @@ export function EditProjectSideModalForm() { title="Edit project" onDismiss={onDismiss} onSubmit={({ name, description }) => { - editProject.mutate({ - ...toPathQuery('project', toApiSelector(projectParams)), - body: { name, description }, - }) + editProject.mutate({ ...projectPathQuery, body: { name, description } }) }} loading={editProject.isLoading} submitError={editProject.error} diff --git a/app/forms/snapshot-create.tsx b/app/forms/snapshot-create.tsx index 1c90ff04b..cf9693dd2 100644 --- a/app/forms/snapshot-create.tsx +++ b/app/forms/snapshot-create.tsx @@ -1,7 +1,7 @@ import { useNavigate } from 'react-router-dom' import type { PathParamsV1, SnapshotCreate } from '@oxide/api' -import { toApiSelector, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' +import { useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' import { @@ -10,8 +10,8 @@ import { NameField, SideModalForm, } from 'app/components/form' -import { useRequiredParams, useToast } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { useProjectSelector, useToast } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' const useSnapshotDiskItems = (projectSelector: PathParamsV1.Project) => { const { data: disks } = useApiQuery('diskListV1', { @@ -32,14 +32,13 @@ const defaultValues: SnapshotCreate = { export function CreateSnapshotSideModalForm() { const queryClient = useApiQueryClient() - const projectParams = useRequiredParams('orgName', 'projectName') - const projectSelector = toApiSelector(projectParams) + const projectSelector = useProjectSelector() const addToast = useToast() const navigate = useNavigate() const diskItems = useSnapshotDiskItems(projectSelector) - const onDismiss = () => navigate(pb.snapshots(projectParams)) + const onDismiss = () => navigate(pb2.snapshots(projectSelector)) const createSnapshot = useApiMutation('snapshotCreateV1', { onSuccess() { diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index 70b25456b..bdad3dd1c 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -37,6 +37,11 @@ export const useSiloParams = () => requireSiloParams(useParams()) export const useSledParams = () => requireSledParams(useParams()) export const useUpdateParams = () => requireUpdateParams(useParams()) +export const getProjectSelector = (p: Readonly>) => + toApiSelector(requireProjectParams(p)) +export const getInstanceSelector = (p: Readonly>) => + toApiSelector(requireInstanceParams(p)) + export const useOrgSelector = () => toApiSelector(useOrgParams()) export const useProjectSelector = () => toApiSelector(useProjectParams()) export const useInstanceSelector = () => toApiSelector(useInstanceParams()) diff --git a/app/pages/project/disks/DisksPage.tsx b/app/pages/project/disks/DisksPage.tsx index 16fed77b1..d792396c4 100644 --- a/app/pages/project/disks/DisksPage.tsx +++ b/app/pages/project/disks/DisksPage.tsx @@ -6,7 +6,6 @@ import type { Disk } from '@oxide/api' import { apiQueryClient, genName, - toApiSelector, useApiMutation, useApiQuery, useApiQueryClient, @@ -23,21 +22,15 @@ import { } from '@oxide/ui' import { DiskStatusBadge } from 'app/components/StatusBadge' -import { - requireProjectParams, - useProjectParams, - useRequiredParams, - useToast, -} from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { getProjectSelector, useProjectSelector, useToast } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' function AttachedInstance({ - orgName, - projectName, instanceId, + ...projectSelector }: { - orgName: string - projectName: string + organization: string + project: string instanceId: string }) { const { data: instance } = useApiQuery('instanceViewV1', { @@ -46,7 +39,7 @@ function AttachedInstance({ return instance ? ( {instance.name} @@ -59,22 +52,20 @@ const EmptyState = () => ( title="No disks" body="You need to create a disk to be able to see it here" buttonText="New disk" - buttonTo={pb.diskNew(useProjectParams())} + buttonTo={pb2.diskNew(useProjectSelector())} /> ) DisksPage.loader = async ({ params }: LoaderFunctionArgs) => { - const projectSelector = toApiSelector(requireProjectParams(params)) await apiQueryClient.prefetchQuery('diskListV1', { - query: { ...projectSelector, limit: 10 }, + query: { ...getProjectSelector(params), limit: 10 }, }) return null } export function DisksPage() { const queryClient = useApiQueryClient() - const projectParams = useRequiredParams('orgName', 'projectName') - const projectSelector = toApiSelector(projectParams) + const projectSelector = useProjectSelector() const { Table, Column } = useQueryTable('diskListV1', { query: projectSelector }) const addToast = useToast() @@ -131,7 +122,7 @@ export function DisksPage() { }>Disks - + New Disk @@ -147,7 +138,7 @@ export function DisksPage() { 'instance' in disk.state ? disk.state.instance : null } cell={({ value }: { value: string | undefined }) => - value ? : null + value ? : null } /> diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index 4de5a77a7..01179a640 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -3,48 +3,45 @@ import { useMemo } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' import { useNavigate } from 'react-router-dom' -import { - apiQueryClient, - toApiSelector, - toPathQuery, - useApiQuery, - useApiQueryClient, -} from '@oxide/api' +import { apiQueryClient, toPathQuery, useApiQuery, useApiQueryClient } from '@oxide/api' import { Instances24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui' -import { pick } from '@oxide/util' import { MoreActionsMenu } from 'app/components/MoreActionsMenu' import { RouteTabs, Tab } from 'app/components/RouteTabs' import { InstanceStatusBadge } from 'app/components/StatusBadge' -import { requireInstanceParams, useQuickActions, useRequiredParams } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { getInstanceSelector, useInstanceSelector, useQuickActions } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' import { useMakeInstanceActions } from '../actions' InstancePage.loader = async ({ params }: LoaderFunctionArgs) => { await apiQueryClient.prefetchQuery( 'instanceViewV1', - toPathQuery('instance', toApiSelector(requireInstanceParams(params))) + toPathQuery('instance', getInstanceSelector(params)) ) return null } export function InstancePage() { - const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') - const instanceSelector = toPathQuery('instance', toApiSelector(instanceParams)) + const instanceSelector = useInstanceSelector() + const { project, organization } = instanceSelector + const instancePathQuery = toPathQuery('instance', instanceSelector) const navigate = useNavigate() const queryClient = useApiQueryClient() - const projectParams = pick(instanceParams, 'projectName', 'orgName') - const makeActions = useMakeInstanceActions(projectParams, { - onSuccess: () => { - queryClient.invalidateQueries('instanceViewV1', instanceSelector) - }, - // go to project instances list since there's no more instance - onDelete: () => navigate(pb.instances(projectParams)), - }) + // TODO: change the interface here to take projectSelector directly + const makeActions = useMakeInstanceActions( + { projectName: project, orgName: organization }, + { + onSuccess: () => { + queryClient.invalidateQueries('instanceViewV1', instancePathQuery) + }, + // go to project instances list since there's no more instance + onDelete: () => navigate(pb2.instances(instanceSelector)), + } + ) - const { data: instance } = useApiQuery('instanceViewV1', instanceSelector) + const { data: instance } = useApiQuery('instanceViewV1', instancePathQuery) const actions = useMemo( () => (instance ? makeActions(instance) : []), [instance, makeActions] @@ -93,10 +90,10 @@ export function InstancePage() { - Storage - Metrics - Network Interfaces - Serial Console + Storage + Metrics + Network Interfaces + Serial Console ) diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index 9fef29f37..974261388 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -4,11 +4,11 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import invariant from 'tiny-invariant' import type { Cumulativeint64, DiskMetricName } from '@oxide/api' -import { apiQueryClient, toApiSelector, toPathQuery, useApiQuery } from '@oxide/api' +import { apiQueryClient, toPathQuery, useApiQuery } from '@oxide/api' import { Listbox, Spinner } from '@oxide/ui' import { useDateTimeRangePicker } from 'app/components/form' -import { requireInstanceParams, useRequiredParams } from 'app/hooks' +import { getInstanceSelector, useInstanceSelector } from 'app/hooks' const TimeSeriesChart = React.lazy(() => import('app/components/TimeSeriesChart')) @@ -74,19 +74,19 @@ function DiskMetric({ // date range, I'm inclined to punt. MetricsTab.loader = async ({ params }: LoaderFunctionArgs) => { - const instanceParams = requireInstanceParams(params) await apiQueryClient.prefetchQuery( 'instanceDiskListV1', - toPathQuery('instance', toApiSelector(instanceParams)) + toPathQuery('instance', getInstanceSelector(params)) ) return null } export function MetricsTab() { - const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') + const instanceSelector = useInstanceSelector() + const { organization, project } = instanceSelector const { data } = useApiQuery( 'instanceDiskListV1', - toPathQuery('instance', toApiSelector(instanceParams)) + toPathQuery('instance', instanceSelector) ) const disks = useMemo(() => data?.items || [], [data]) @@ -94,14 +94,12 @@ export function MetricsTab() { // have a disk, we should never see an empty list here invariant(disks.length > 0, 'Instance disks list should never be empty') - const { orgName, projectName } = instanceParams - const { startTime, endTime, dateTimeRangePicker } = useDateTimeRangePicker('lastDay') const [diskName, setDiskName] = useState(disks[0].name) const diskItems = disks.map(({ name }) => ({ label: name, value: name })) - const diskParams = { orgName, projectName, diskName } + const diskParams = { orgName: organization, projectName: project, diskName } const commonProps = { startTime: startTime.toDate(getLocalTimeZone()), endTime: endTime.toDate(getLocalTimeZone()), diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 3e56742c5..3aed2f647 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -5,7 +5,6 @@ import { Link } from 'react-router-dom' import type { NetworkInterface } from '@oxide/api' import { apiQueryClient, - toApiSelector, toPathQuery, useApiMutation, useApiQuery, @@ -25,7 +24,12 @@ import { import CreateNetworkInterfaceForm from 'app/forms/network-interface-create' import EditNetworkInterfaceForm from 'app/forms/network-interface-edit' -import { requireInstanceParams, useRequiredParams, useToast } from 'app/hooks' +import { + getInstanceSelector, + useInstanceSelector, + useRequiredParams, + useToast, +} from 'app/hooks' import { pb } from 'app/util/path-builder' const VpcNameFromId = ({ value }: { value: string }) => { @@ -56,7 +60,7 @@ function ExternalIpsFromInstanceName({ value: primary }: { value: boolean }) { } NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { - const instanceSelector = toApiSelector(requireInstanceParams(params)) + const instanceSelector = getInstanceSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('instanceNetworkInterfaceListV1', { query: { ...instanceSelector, limit: 10 }, @@ -72,8 +76,7 @@ NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { } export function NetworkingTab() { - const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') - const instanceSelector = toApiSelector(instanceParams) + const instanceSelector = useInstanceSelector() const queryClient = useApiQueryClient() const addToast = useToast() diff --git a/app/pages/project/instances/instance/tabs/StorageTab.tsx b/app/pages/project/instances/instance/tabs/StorageTab.tsx index 855a4ca08..b866fa492 100644 --- a/app/pages/project/instances/instance/tabs/StorageTab.tsx +++ b/app/pages/project/instances/instance/tabs/StorageTab.tsx @@ -4,7 +4,6 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { type Disk, apiQueryClient, - toApiSelector, toPathQuery, useApiMutation, useApiQuery, @@ -24,7 +23,7 @@ import { Button, EmptyMessage, Error16Icon, OpenLink12Icon, TableEmptyBox } from import { DiskStatusBadge } from 'app/components/StatusBadge' import AttachDiskSideModalForm from 'app/forms/disk-attach' import { CreateDiskSideModalForm } from 'app/forms/disk-create' -import { requireInstanceParams, useRequiredParams, useToast } from 'app/hooks' +import { getInstanceSelector, useInstanceSelector, useToast } from 'app/hooks' const OtherDisksEmpty = () => ( @@ -58,13 +57,12 @@ const staticCols = [ ] StorageTab.loader = async ({ params }: LoaderFunctionArgs) => { - const instanceParams = requireInstanceParams(params) - const instanceSelector = toPathQuery('instance', toApiSelector(instanceParams)) + const instancePathQuery = toPathQuery('instance', getInstanceSelector(params)) await Promise.all([ - apiQueryClient.prefetchQuery('instanceDiskListV1', instanceSelector), + apiQueryClient.prefetchQuery('instanceDiskListV1', instancePathQuery), // This is covered by the InstancePage loader but there's no downside to // being redundant. If it were removed there, we'd still want it here. - apiQueryClient.prefetchQuery('instanceViewV1', instanceSelector), + apiQueryClient.prefetchQuery('instanceViewV1', instancePathQuery), ]) return null } @@ -75,15 +73,14 @@ export function StorageTab() { const addToast = useToast() const queryClient = useApiQueryClient() - const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') - const instanceSelector = toPathQuery('instance', toApiSelector(instanceParams)) + const instancePathQuery = toPathQuery('instance', useInstanceSelector()) - const { data } = useApiQuery('instanceDiskListV1', instanceSelector) + const { data } = useApiQuery('instanceDiskListV1', instancePathQuery) const detachDisk = useApiMutation('instanceDiskDetachV1', {}) const instanceStopped = - useApiQuery('instanceViewV1', instanceSelector).data?.runState === 'stopped' + useApiQuery('instanceViewV1', instancePathQuery).data?.runState === 'stopped' const makeActions = useCallback( (disk: Disk): MenuAction[] => [ @@ -93,23 +90,23 @@ export function StorageTab() { !instanceStopped && 'Instance must be stopped before disk can be detached', onActivate() { detachDisk.mutate( - { body: { disk: disk.name }, ...instanceSelector }, + { body: { disk: disk.name }, ...instancePathQuery }, { onSuccess: () => { - queryClient.invalidateQueries('instanceDiskListV1', instanceSelector) + queryClient.invalidateQueries('instanceDiskListV1', instancePathQuery) }, } ) }, }, ], - [detachDisk, instanceStopped, queryClient, instanceSelector] + [detachDisk, instanceStopped, queryClient, instancePathQuery] ) const attachDisk = useApiMutation('instanceDiskAttachV1', { onSuccess() { console.log('disk atttach success') - queryClient.invalidateQueries('instanceDiskListV1', instanceSelector) + queryClient.invalidateQueries('instanceDiskListV1', instancePathQuery) }, onError(err) { addToast({ @@ -191,7 +188,7 @@ export function StorageTab() { onSuccess={({ name }) => { // TODO: this should probably be done with `mutateAsync` and // awaited, but it's a pain, so punt for now - attachDisk.mutate({ ...instanceSelector, body: { disk: name } }) + attachDisk.mutate({ ...instancePathQuery, body: { disk: name } }) }} /> )} diff --git a/app/pages/project/snapshots/SnapshotsPage.tsx b/app/pages/project/snapshots/SnapshotsPage.tsx index 1bd0f224b..01ead7849 100644 --- a/app/pages/project/snapshots/SnapshotsPage.tsx +++ b/app/pages/project/snapshots/SnapshotsPage.tsx @@ -2,7 +2,6 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { Link, Outlet } from 'react-router-dom' import type { Snapshot } from '@oxide/api' -import { toApiSelector } from '@oxide/api' import { apiQueryClient, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import type { MenuAction } from '@oxide/table' import { DateCell, SizeCell, useQueryTable } from '@oxide/table' @@ -16,8 +15,8 @@ import { } from '@oxide/ui' import { SnapshotStatusBadge } from 'app/components/StatusBadge' -import { requireProjectParams, useProjectParams, useRequiredParams } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { getProjectSelector, useProjectSelector } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' const DiskNameFromId = ({ value }: { value: string }) => { const { data: disk } = useApiQuery('diskViewV1', { path: { disk: value } }) @@ -31,22 +30,20 @@ const EmptyState = () => ( title="No snapshots" body="You need to create a snapshot to be able to see it here" buttonText="New snapshot" - buttonTo={pb.snapshotNew(useProjectParams())} + buttonTo={pb2.snapshotNew(useProjectSelector())} /> ) SnapshotsPage.loader = async ({ params }: LoaderFunctionArgs) => { - const projectSelector = toApiSelector(requireProjectParams(params)) await apiQueryClient.prefetchQuery('snapshotListV1', { - query: { ...projectSelector, limit: 10 }, + query: { ...getProjectSelector(params), limit: 10 }, }) return null } export function SnapshotsPage() { const queryClient = useApiQueryClient() - const projectParams = useRequiredParams('orgName', 'projectName') - const projectSelector = toApiSelector(projectParams) + const projectSelector = useProjectSelector() const { Table, Column } = useQueryTable('snapshotListV1', { query: projectSelector }) const deleteSnapshot = useApiMutation('snapshotDeleteV1', { @@ -70,7 +67,7 @@ export function SnapshotsPage() { }>Snapshots - + New Snapshot diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 3b4cb391a..bcf4f6e94 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -1,4 +1,4 @@ -import type { PathParams as PP } from '@oxide/api' +import type { PathParams as PP, PathParamsV1 as PPv1 } from '@oxide/api' export const pb = { orgs: () => '/orgs', @@ -82,3 +82,94 @@ export const pb = { } // export const jelly = 'just kidding' + +// TODO: required versions of path params probably belong somewhere else, +// they're useful + +type Org = Required +type Project = Required +type Instance = Required +type Vpc = Required +type SystemUpdate = Required +type Silo = Required + +// TODO: obviously the plan is pb2 becomes pb +export const pb2 = { + orgs: () => '/orgs', + orgNew: () => '/orgs-new', + org: ({ organization }: Org) => `${pb2.orgs()}/${organization}`, + orgEdit: (params: Org) => `${pb2.org(params)}/edit`, + orgAccess: (params: Org) => `${pb2.org(params)}/access`, + + projects: (params: Org) => `${pb2.org(params)}/projects`, + projectNew: (params: Org) => `${pb2.org(params)}/projects-new`, + project: ({ organization, project }: Project) => + `${pb2.projects({ organization })}/${project}`, + projectEdit: (params: Project) => `${pb2.project(params)}/edit`, + + projectAccess: (params: Project) => `${pb2.project(params)}/access`, + projectImages: (params: Project) => `${pb2.project(params)}/images`, + + instances: (params: Project) => `${pb2.project(params)}/instances`, + instanceNew: (params: Project) => `${pb2.project(params)}/instances-new`, + instance: (params: Instance) => `${pb2.instances(params)}/${params.instance}`, + + /** + * This route exists as a direct link to the default tab of the instance page. Unfortunately + * we don't currently have a good mechanism at the moment to handle a redirect to the default + * tab in a seemless way so we need all in-app links to go directly to the default tab. + * + * @see https://github.com/oxidecomputer/console/pull/1267#discussion_r1016766205 + */ + instancePage: (params: Instance) => pb2.instanceStorage(params), + + instanceMetrics: (params: Instance) => `${pb2.instance(params)}/metrics`, + instanceStorage: (params: Instance) => `${pb2.instance(params)}/storage`, + + nics: (params: Instance) => `${pb2.instance(params)}/network-interfaces`, + + serialConsole: (params: Instance) => `${pb2.instance(params)}/serial-console`, + + diskNew: (params: Project) => `${pb2.project(params)}/disks-new`, + disks: (params: Project) => `${pb2.project(params)}/disks`, + + snapshotNew: (params: Project) => `${pb2.project(params)}/snapshots-new`, + snapshots: (params: Project) => `${pb2.project(params)}/snapshots`, + + vpcNew: (params: Project) => `${pb2.project(params)}/vpcs-new`, + vpcs: (params: Project) => `${pb2.project(params)}/vpcs`, + vpc: (params: Vpc) => `${pb2.vpcs(params)}/${params.vpc}`, + vpcEdit: (params: Vpc) => `${pb2.vpc(params)}/edit`, + + siloUtilization: () => '/utilization', + siloAccess: () => '/access', + + system: () => '/sys', + systemIssues: () => '/sys/issues', + systemUtilization: () => '/sys/utilization', + systemHealth: () => '/sys/health', + + systemUpdates: () => '/sys/update/updates', + systemUpdateDetail: ({ version }: SystemUpdate) => `${pb2.systemUpdates()}/${version}`, + systemUpdateHistory: () => '/sys/update/history', + updateableComponents: () => '/sys/update/components', + + systemNetworking: () => '/sys/networking', + systemSettings: () => '/sys/settings', + + rackInventory: () => '/sys/inventory/racks', + sledInventory: () => '/sys/inventory/sleds', + diskInventory: () => '/sys/inventory/disks', + + silos: () => '/sys/silos', + siloNew: () => '/sys/silos-new', + silo: ({ silo }: Silo) => `/sys/silos/${silo}`, + siloIdpNew: (params: Silo) => `${pb2.silo(params)}/idps-new`, + + settings: () => '/settings', + profile: () => '/settings/profile', + sshKeys: () => '/settings/ssh-keys', + sshKeyNew: () => '/settings/ssh-keys-new', + + deviceSuccess: () => '/device/success', +} diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index e900ef5d8..512448525 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -11,5 +11,6 @@ export type Snapshot = Merge export type Vpc = Merge export type VpcSubnet = Merge export type SystemUpdate = { version: string } +export type Silo = { silo: string } export type Id = { id: string } From 3ea9969661ecf2c1b215bcc410e6daa440f8124b Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 10:25:34 -0600 Subject: [PATCH 21/39] instances list, no more uses of useProjectParams --- app/components/TopBarPicker.tsx | 23 ++++---- app/forms/disk-attach.tsx | 6 +-- app/forms/instance-create.tsx | 27 +++++----- app/hooks/use-params.ts | 2 +- app/pages/project/instances/InstancesPage.tsx | 53 +++++++++---------- app/pages/project/networking/VpcsPage.tsx | 6 +-- libs/api-mocks/msw/handlers.ts | 8 +-- 7 files changed, 62 insertions(+), 63 deletions(-) diff --git a/app/components/TopBarPicker.tsx b/app/components/TopBarPicker.tsx index 9c6c1b29f..419961512 100644 --- a/app/components/TopBarPicker.tsx +++ b/app/components/TopBarPicker.tsx @@ -5,8 +5,8 @@ import { Link, useParams } from 'react-router-dom' import { useApiQuery } from '@oxide/api' import { Identicon, Organization16Icon, SelectArrows6Icon, Success12Icon } from '@oxide/ui' -import { useInstanceParams, useProjectParams, useSiloParams } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { useInstanceSelector, useProjectSelector, useSiloParams } from 'app/hooks' +import { pb, pb2 } from 'app/util/path-builder' type TopBarPickerItem = { label: string @@ -194,20 +194,20 @@ export function OrgPicker() { export function ProjectPicker() { // picker only shows up when a project is in scope - const { orgName, projectName } = useProjectParams() + const { organization, project } = useProjectSelector() const { data } = useApiQuery('projectListV1', { - query: { organization: orgName, limit: 20 }, + query: { organization, limit: 20 }, }) const items = (data?.items || []).map(({ name }) => ({ label: name, - to: pb.instances({ orgName, projectName: name }), + to: pb2.instances({ organization, project: name }), })) return ( @@ -216,21 +216,20 @@ export function ProjectPicker() { export function InstancePicker() { // picker only shows up when an instance is in scope - const { orgName, projectName, instanceName } = useInstanceParams() - const { data } = useApiQuery('instanceList', { - path: { orgName, projectName }, - query: { limit: 50 }, + const { organization, project, instance } = useInstanceSelector() + const { data } = useApiQuery('instanceListV1', { + query: { organization, project, limit: 50 }, }) const items = (data?.items || []).map(({ name }) => ({ label: name, - to: pb.instance({ orgName, projectName, instanceName: name }), + to: pb2.instance({ organization, project, instance: name }), })) return ( diff --git a/app/forms/disk-attach.tsx b/app/forms/disk-attach.tsx index 375220313..f05126e05 100644 --- a/app/forms/disk-attach.tsx +++ b/app/forms/disk-attach.tsx @@ -2,10 +2,10 @@ import { useParams } from 'react-router-dom' import invariant from 'tiny-invariant' import type { Disk, DiskIdentifier } from '@oxide/api' -import { toApiSelector, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' +import { useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import { ListboxField, SideModalForm } from 'app/components/form' -import { useProjectParams } from 'app/hooks' +import { useProjectSelector } from 'app/hooks' const defaultValues = { name: '' } @@ -25,7 +25,7 @@ export function AttachDiskSideModalForm({ // instance name undefined when this form is called from DisksTableField on // instance create, which passes in its own onSubmit, bypassing the attachDisk mutation const { instanceName } = useParams() - const projectSelector = toApiSelector(useProjectParams()) + const projectSelector = useProjectSelector() const attachDisk = useApiMutation('instanceDiskAttachV1', { onSuccess(data) { diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 944337517..9c51f5f57 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -3,11 +3,13 @@ import invariant from 'tiny-invariant' import type { SetRequired } from 'type-fest' import type { InstanceCreate } from '@oxide/api' -import { toApiSelector } from '@oxide/api' -import { apiQueryClient } from '@oxide/api' -import { genName } from '@oxide/api' -import { useApiQuery } from '@oxide/api' -import { useApiMutation, useApiQueryClient } from '@oxide/api' +import { + apiQueryClient, + genName, + useApiMutation, + useApiQuery, + useApiQueryClient, +} from '@oxide/api' import { Divider, Instances24Icon, @@ -32,8 +34,8 @@ import { RadioFieldDyn, TextField, } from 'app/components/form' -import { useRequiredParams, useToast } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { useProjectSelector, useToast } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' export type InstanceCreateInput = Assign< // API accepts undefined but it's easier if we don't @@ -77,14 +79,13 @@ CreateInstanceForm.loader = async () => { export function CreateInstanceForm() { const queryClient = useApiQueryClient() const addToast = useToast() - const projectParams = useRequiredParams('orgName', 'projectName') - const projectSelector = toApiSelector(projectParams) + const projectSelector = useProjectSelector() const navigate = useNavigate() const createInstance = useApiMutation('instanceCreateV1', { onSuccess(instance) { // refetch list of instances - queryClient.invalidateQueries('instanceList', { path: projectParams }) + queryClient.invalidateQueries('instanceListV1', { query: projectSelector }) // avoid the instance fetch when the instance page loads since we have the data queryClient.setQueryData( 'instanceViewV1', @@ -96,7 +97,7 @@ export function CreateInstanceForm() { title: 'Success!', content: 'Your instance has been created.', }) - navigate(pb.instancePage({ ...projectParams, instanceName: instance.name })) + navigate(pb2.instancePage({ ...projectSelector, instance: instance.name })) }, }) @@ -134,7 +135,7 @@ export function CreateInstanceForm() { body: { name: values.name, hostname: values.hostname || values.name, - description: `An instance in project: ${projectParams.projectName}`, + description: `An instance in project: ${projectSelector.project}`, memory: instance.memory * GiB, ncpus: instance.ncpus, disks: [ @@ -265,7 +266,7 @@ export function CreateInstanceForm() { Create instance - navigate(pb.instances(projectParams))} /> + navigate(pb2.instances(projectSelector))} /> )} diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index bdad3dd1c..0f467bd16 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -30,7 +30,7 @@ export const requireSledParams = requireParams('sledId') export const requireUpdateParams = requireParams('version') export const useOrgParams = () => requireOrgParams(useParams()) -export const useProjectParams = () => requireProjectParams(useParams()) +const useProjectParams = () => requireProjectParams(useParams()) export const useInstanceParams = () => requireInstanceParams(useParams()) export const useVpcParams = () => requireVpcParams(useParams()) export const useSiloParams = () => requireSiloParams(useParams()) diff --git a/app/pages/project/instances/InstancesPage.tsx b/app/pages/project/instances/InstancesPage.tsx index 80cd7c95c..8743c653f 100644 --- a/app/pages/project/instances/InstancesPage.tsx +++ b/app/pages/project/instances/InstancesPage.tsx @@ -21,8 +21,8 @@ import { buttonStyle, } from '@oxide/ui' -import { requireProjectParams, useProjectParams, useQuickActions } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' import { useMakeInstanceActions } from './actions' @@ -32,54 +32,58 @@ const EmptyState = () => ( title="No instances" body="You need to create an instance to be able to see it here" buttonText="New instance" - buttonTo={pb.instanceNew(useProjectParams())} + buttonTo={pb2.instanceNew(useProjectSelector())} /> ) InstancesPage.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('instanceList', { - path: requireProjectParams(params), - query: { limit: 10 }, + await apiQueryClient.prefetchQuery('instanceListV1', { + query: { ...getProjectSelector(params), limit: 10 }, }) return null } export function InstancesPage() { - const projectParams = useProjectParams() - const { orgName, projectName } = projectParams + const projectSelector = useProjectSelector() + const { organization, project } = projectSelector const queryClient = useApiQueryClient() const refetchInstances = () => - queryClient.invalidateQueries('instanceList', { path: projectParams }) + queryClient.invalidateQueries('instanceListV1', { query: projectSelector }) - const makeActions = useMakeInstanceActions(projectParams, { - onSuccess: refetchInstances, - }) + const makeActions = useMakeInstanceActions( + { orgName: organization, projectName: project }, + { + onSuccess: refetchInstances, + } + ) - const { data: instances } = useApiQuery('instanceList', { - path: projectParams, - query: { limit: 10 }, // to have same params as QueryTable + const { data: instances } = useApiQuery('instanceListV1', { + query: { ...projectSelector, limit: 10 }, // to have same params as QueryTable }) const navigate = useNavigate() useQuickActions( useMemo( () => [ - { value: 'New instance', onSelect: () => navigate(pb.instanceNew(projectParams)) }, + { + value: 'New instance', + onSelect: () => navigate(pb2.instanceNew(projectSelector)), + }, ...(instances?.items || []).map((i) => ({ value: i.name, onSelect: () => - navigate(pb.instancePage({ ...projectParams, instanceName: i.name })), + navigate(pb2.instancePage({ ...projectSelector, instance: i.name })), navGroup: 'Go to instance', })), ], - [projectParams, instances, navigate] + [projectSelector, instances, navigate] ) ) const { Table, Column } = useQueryTable( - 'instanceList', - { path: projectParams }, + 'instanceListV1', + { query: projectSelector }, { keepPreviousData: true } ) @@ -94,19 +98,14 @@ export function InstancesPage() { - + New Instance }> - pb.instancePage({ orgName, projectName, instanceName }) - )} + cell={linkCell((instance) => pb2.instancePage({ ...projectSelector, instance }))} /> ( ( title="No VPCs" body="You need to create a VPC to be able to see it here" buttonText="New VPC" - buttonTo={pb.vpcNew(useProjectParams())} + buttonTo={pb2.vpcNew(useProjectSelector())} /> ) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 4cf53e7cd..842c2b2b1 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -238,10 +238,10 @@ export const handlers = makeHandlers({ return 204 }, - instanceList(params) { - const project = lookupProject(params.path) + instanceListV1({ query }) { + const project = lookup.project(query) const instances = db.instances.filter((i) => i.project_id === project.id) - return paginated(params.query, instances) + return paginated(query, instances) }, instanceCreateV1({ body, query }) { const project = lookup.project(query) @@ -1043,7 +1043,6 @@ export const handlers = makeHandlers({ certificateListV1: NotImplemented, certificateViewV1: NotImplemented, instanceDeleteV1: NotImplemented, - instanceListV1: NotImplemented, instanceMigrateV1: NotImplemented, instanceNetworkInterfaceCreateV1: NotImplemented, instanceRebootV1: NotImplemented, @@ -1114,6 +1113,7 @@ export const handlers = makeHandlers({ instanceDiskAttach: NotImplemented, instanceDiskDetach: NotImplemented, instanceDiskList: NotImplemented, + instanceList: NotImplemented, instanceNetworkInterfaceDelete: NotImplemented, instanceNetworkInterfaceList: NotImplemented, instanceNetworkInterfaceUpdate: NotImplemented, From 2eeb794675845d369e15140ca0ec1743b7ed787c Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 11:26:08 -0600 Subject: [PATCH 22/39] vpc endpoints --- app/forms/network-interface-create.tsx | 10 ++--- app/forms/vpc-create.tsx | 22 +++++----- app/forms/vpc-edit.tsx | 33 ++++++++------- app/hooks/use-params.ts | 7 +++- .../project/networking/VpcPage/VpcPage.tsx | 13 +++--- app/pages/project/networking/VpcsPage.tsx | 42 +++++++------------ libs/api-mocks/msw/db.ts | 1 + libs/api-mocks/msw/handlers.ts | 28 ++++++------- 8 files changed, 78 insertions(+), 78 deletions(-) diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index f70fe677d..e743773de 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -2,8 +2,7 @@ import { useMemo } from 'react' import invariant from 'tiny-invariant' import type { NetworkInterfaceCreate } from '@oxide/api' -import { useApiQuery } from '@oxide/api' -import { useApiMutation, useApiQueryClient } from '@oxide/api' +import { useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import { Divider } from '@oxide/ui' import { @@ -14,7 +13,7 @@ import { SubnetListbox, TextField, } from 'app/components/form' -import { useAllParams } from 'app/hooks' +import { useAllParams, useProjectSelector } from 'app/hooks' const defaultValues: NetworkInterfaceCreate = { name: '', @@ -35,18 +34,19 @@ export default function CreateNetworkInterfaceForm({ }: CreateNetworkInterfaceFormProps) { const queryClient = useApiQueryClient() const { orgName, projectName, instanceName } = useAllParams('orgName', 'projectName') + const projectSelector = useProjectSelector() const createNetworkInterface = useApiMutation('instanceNetworkInterfaceCreate', { onSuccess() { invariant(instanceName, 'instanceName is required when posting a network interface') queryClient.invalidateQueries('instanceNetworkInterfaceListV1', { - query: { instance: instanceName, project: projectName, organization: orgName }, + query: { instance: instanceName, ...projectSelector }, }) onDismiss() }, }) - const { data: vpcsData } = useApiQuery('vpcList', { path: { orgName, projectName } }) + const { data: vpcsData } = useApiQuery('vpcListV1', { query: projectSelector }) const vpcs = useMemo(() => vpcsData?.items || [], [vpcsData]) return ( diff --git a/app/forms/vpc-create.tsx b/app/forms/vpc-create.tsx index 5b4bdbb54..2980e7b75 100644 --- a/app/forms/vpc-create.tsx +++ b/app/forms/vpc-create.tsx @@ -1,12 +1,12 @@ import { useNavigate } from 'react-router-dom' import type { VpcCreate } from '@oxide/api' -import { useApiMutation, useApiQueryClient } from '@oxide/api' +import { toPathQuery, useApiMutation, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm, TextField } from 'app/components/form' -import { useRequiredParams, useToast } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { useProjectSelector, useToast } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' const defaultValues: VpcCreate = { name: '', @@ -15,23 +15,23 @@ const defaultValues: VpcCreate = { } export function CreateVpcSideModalForm() { - const parentNames = useRequiredParams('orgName', 'projectName') + const projectSelector = useProjectSelector() const queryClient = useApiQueryClient() const addToast = useToast() const navigate = useNavigate() - const createVpc = useApiMutation('vpcCreate', { + const createVpc = useApiMutation('vpcCreateV1', { onSuccess(vpc) { - const vpcParams = { ...parentNames, vpcName: vpc.name } - queryClient.invalidateQueries('vpcList', { path: parentNames }) + const vpcSelector = { ...projectSelector, vpc: vpc.name } + queryClient.invalidateQueries('vpcListV1', { query: projectSelector }) // avoid the vpc fetch when the vpc page loads since we have the data - queryClient.setQueryData('vpcView', { path: vpcParams }, vpc) + queryClient.setQueryData('vpcViewV1', toPathQuery('vpc', vpcSelector), vpc) addToast({ icon: , title: 'Success!', content: 'Your VPC has been created.', }) - navigate(pb.vpc(vpcParams)) + navigate(pb2.vpc(vpcSelector)) }, }) @@ -40,8 +40,8 @@ export function CreateVpcSideModalForm() { id="create-vpc-form" title="Create VPC" formOptions={{ defaultValues }} - onSubmit={(values) => createVpc.mutate({ path: parentNames, body: values })} - onDismiss={() => navigate(pb.vpcs(parentNames))} + onSubmit={(values) => createVpc.mutate({ query: projectSelector, body: values })} + onDismiss={() => navigate(pb2.vpcs(projectSelector))} loading={createVpc.isLoading} submitError={createVpc.error} > diff --git a/app/forms/vpc-edit.tsx b/app/forms/vpc-edit.tsx index 52a40b66e..e535665db 100644 --- a/app/forms/vpc-edit.tsx +++ b/app/forms/vpc-edit.tsx @@ -1,38 +1,41 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { useNavigate } from 'react-router-dom' -import { useApiQuery } from '@oxide/api' +import { toPathQuery, useApiQuery } from '@oxide/api' import { apiQueryClient, useApiMutation, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' +import { pick } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { requireVpcParams, useToast, useVpcParams } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { getVpcSelector, useToast, useVpcSelector } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' EditVpcSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('vpcView', { - path: requireVpcParams(params), - }) + await apiQueryClient.prefetchQuery( + 'vpcViewV1', + toPathQuery('vpc', getVpcSelector(params)) + ) return null } export function EditVpcSideModalForm() { - const vpcParams = useVpcParams() - const parentNames = { orgName: vpcParams.orgName, projectName: vpcParams.projectName } + const vpcSelector = useVpcSelector() + const vpcPathQuery = toPathQuery('vpc', vpcSelector) + const projectSelector = pick(vpcSelector, 'organization', 'project') const queryClient = useApiQueryClient() const addToast = useToast() const navigate = useNavigate() - const { data: vpc } = useApiQuery('vpcView', { path: vpcParams }) + const { data: vpc } = useApiQuery('vpcViewV1', vpcPathQuery) - const onDismiss = () => navigate(pb.vpcs(parentNames)) + const onDismiss = () => navigate(pb2.vpcs(projectSelector)) - const editVpc = useApiMutation('vpcUpdate', { + const editVpc = useApiMutation('vpcUpdateV1', { async onSuccess(vpc) { - queryClient.invalidateQueries('vpcList', { path: parentNames }) + queryClient.invalidateQueries('vpcListV1', { query: projectSelector }) queryClient.setQueryData( - 'vpcView', - { path: { ...parentNames, vpcName: vpc.name } }, + 'vpcViewV1', + { path: { vpc: vpc.name }, query: projectSelector }, vpc ) addToast({ @@ -52,7 +55,7 @@ export function EditVpcSideModalForm() { onDismiss={onDismiss} onSubmit={({ name, description, dnsName }) => { editVpc.mutate({ - path: vpcParams, + ...vpcPathQuery, body: { name, description, dnsName }, }) }} diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index 0f467bd16..ea04e8984 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -24,7 +24,7 @@ export const requireParams = 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') +const requireVpcParams = requireParams('orgName', 'projectName', 'vpcName') export const requireSiloParams = requireParams('siloName') export const requireSledParams = requireParams('sledId') export const requireUpdateParams = requireParams('version') @@ -32,7 +32,7 @@ export const requireUpdateParams = requireParams('version') export const useOrgParams = () => requireOrgParams(useParams()) const useProjectParams = () => requireProjectParams(useParams()) export const useInstanceParams = () => requireInstanceParams(useParams()) -export const useVpcParams = () => requireVpcParams(useParams()) +const useVpcParams = () => requireVpcParams(useParams()) export const useSiloParams = () => requireSiloParams(useParams()) export const useSledParams = () => requireSledParams(useParams()) export const useUpdateParams = () => requireUpdateParams(useParams()) @@ -41,9 +41,12 @@ export const getProjectSelector = (p: Readonly>) => toApiSelector(requireProjectParams(p)) export const getInstanceSelector = (p: Readonly>) => toApiSelector(requireInstanceParams(p)) +export const getVpcSelector = (p: Readonly>) => + toApiSelector(requireVpcParams(p)) export const useOrgSelector = () => toApiSelector(useOrgParams()) export const useProjectSelector = () => toApiSelector(useProjectParams()) +export const useVpcSelector = () => toApiSelector(useVpcParams()) export const useInstanceSelector = () => toApiSelector(useInstanceParams()) /** diff --git a/app/pages/project/networking/VpcPage/VpcPage.tsx b/app/pages/project/networking/VpcPage/VpcPage.tsx index 4b4778712..f1cbfdb59 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.tsx +++ b/app/pages/project/networking/VpcPage/VpcPage.tsx @@ -1,11 +1,11 @@ import type { LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, useApiQuery } from '@oxide/api' +import { apiQueryClient, toPathQuery, useApiQuery } from '@oxide/api' import { Networking24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui' import { formatDateTime } from '@oxide/util' import { Tab, Tabs } from 'app/components/Tabs' -import { requireVpcParams, useVpcParams } from 'app/hooks' +import { getVpcSelector, useVpcSelector } from 'app/hooks' import { VpcFirewallRulesTab } from './tabs/VpcFirewallRulesTab' import { VpcRoutersTab } from './tabs/VpcRoutersTab' @@ -13,13 +13,16 @@ import { VpcSubnetsTab } from './tabs/VpcSubnetsTab' import { VpcSystemRoutesTab } from './tabs/VpcSystemRoutesTab' VpcPage.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('vpcView', { path: requireVpcParams(params) }) + await apiQueryClient.prefetchQuery( + 'vpcViewV1', + toPathQuery('vpc', getVpcSelector(params)) + ) return null } export function VpcPage() { - const vpcParams = useVpcParams() - const { data: vpc } = useApiQuery('vpcView', { path: vpcParams }) + const vpcSelector = useVpcSelector() + const { data: vpc } = useApiQuery('vpcViewV1', toPathQuery('vpc', vpcSelector)) return ( <> diff --git a/app/pages/project/networking/VpcsPage.tsx b/app/pages/project/networking/VpcsPage.tsx index ddb78baa5..24e668075 100644 --- a/app/pages/project/networking/VpcsPage.tsx +++ b/app/pages/project/networking/VpcsPage.tsx @@ -16,13 +16,8 @@ import { buttonStyle, } from '@oxide/ui' -import { - requireProjectParams, - useProjectSelector, - useQuickActions, - useRequiredParams, -} from 'app/hooks' -import { pb, pb2 } from 'app/util/path-builder' +import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' const EmptyState = () => ( ( // just as in the vpcList call for the quick actions menu, include limit: 10 to make // sure it matches the call in the QueryTable VpcsPage.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('vpcList', { - path: requireProjectParams(params), - query: { limit: 10 }, + await apiQueryClient.prefetchQuery('vpcListV1', { + query: { ...getProjectSelector(params), limit: 10 }, }) return null } export function VpcsPage() { const queryClient = useApiQueryClient() - const { orgName, projectName } = useRequiredParams('orgName', 'projectName') - const { data: vpcs } = useApiQuery('vpcList', { - path: { orgName, projectName }, - query: { limit: 10 }, // to have same params as QueryTable + const projectSelector = useProjectSelector() + const { data: vpcs } = useApiQuery('vpcListV1', { + query: { ...projectSelector, limit: 10 }, // to have same params as QueryTable }) const navigate = useNavigate() - const deleteVpc = useApiMutation('vpcDelete', { + const deleteVpc = useApiMutation('vpcDeleteV1', { onSuccess() { - queryClient.invalidateQueries('vpcList', { path: { orgName, projectName } }) + queryClient.invalidateQueries('vpcListV1', { query: projectSelector }) }, }) @@ -63,13 +56,13 @@ export function VpcsPage() { { label: 'Edit', onActivate() { - navigate(pb.vpcEdit({ orgName, projectName, vpcName: vpc.name }), { state: vpc }) + navigate(pb2.vpcEdit({ ...projectSelector, vpc: vpc.name }), { state: vpc }) }, }, { label: 'Delete', onActivate() { - deleteVpc.mutate({ path: { orgName, projectName, vpcName: vpc.name } }) + deleteVpc.mutate({ path: { vpc: vpc.name }, query: projectSelector }) }, }, ] @@ -79,31 +72,28 @@ export function VpcsPage() { () => (vpcs?.items || []).map((v) => ({ value: v.name, - onSelect: () => navigate(pb.vpc({ orgName, projectName, vpcName: v.name })), + onSelect: () => navigate(pb2.vpc({ ...projectSelector, vpc: v.name })), navGroup: 'Go to VPC', })), - [orgName, projectName, vpcs, navigate] + [projectSelector, vpcs, navigate] ) ) - const { Table, Column } = useQueryTable('vpcList', { path: { orgName, projectName } }) + const { Table, Column } = useQueryTable('vpcListV1', { query: projectSelector }) return ( <> }>VPCs - + New Vpc
} makeActions={makeActions}> pb.vpc({ orgName, projectName, vpcName }))} + cell={linkCell((vpc) => pb2.vpc({ ...projectSelector, vpc }))} /> diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 0ba597c7a..6f3cf468f 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -93,6 +93,7 @@ export const lookup = { return snapshot }, vpc({ vpc: id, ...projectSelector }: PPv1.Vpc): Json { + console.log({ id, ...projectSelector }) if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.vpcs, id) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 842c2b2b1..a79b7edad 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -542,13 +542,13 @@ export const handlers = makeHandlers({ db.snapshots = db.snapshots.filter((s) => s.id !== snapshot.id) return 204 }, - vpcList(params) { - const project = lookupProject(params.path) + vpcListV1({ query }) { + const project = lookup.project(query) const vpcs = db.vpcs.filter((v) => v.project_id === project.id) - return paginated(params.query, vpcs) + return paginated(query, vpcs) }, - vpcCreate({ body, ...params }) { - const project = lookupProject(params.path) + vpcCreateV1({ body, query }) { + const project = lookup.project(query) errIfExists(db.vpcs, { name: body.name }) const newVpc: Json = { @@ -576,10 +576,9 @@ export const handlers = makeHandlers({ return json(newVpc, { status: 201 }) }, - vpcView: (params) => lookupVpc(params.path), vpcViewV1: ({ path, query }) => lookup.vpc({ ...path, ...query }), - vpcUpdate({ body, ...params }) { - const vpc = lookupVpc(params.path) + vpcUpdateV1({ body, path, query }) { + const vpc = lookup.vpc({ ...path, ...query }) if (body.name) { vpc.name = body.name @@ -594,8 +593,8 @@ export const handlers = makeHandlers({ } return vpc }, - vpcDelete(params) { - const vpc = lookupVpc(params.path) + vpcDeleteV1({ path, query }) { + const vpc = lookup.vpc({ ...path, ...query }) db.vpcs = db.vpcs.filter((v) => v.id !== vpc.id) db.vpcSubnets = db.vpcSubnets.filter((s) => s.vpc_id !== vpc.id) @@ -1068,9 +1067,6 @@ export const handlers = makeHandlers({ sledViewV1: NotImplemented, systemPolicyUpdateV1: NotImplemented, systemPolicyViewV1: NotImplemented, - vpcCreateV1: NotImplemented, - vpcDeleteV1: NotImplemented, - vpcListV1: NotImplemented, vpcRouterCreateV1: NotImplemented, vpcRouterDeleteV1: NotImplemented, vpcRouterListV1: NotImplemented, @@ -1085,7 +1081,6 @@ export const handlers = makeHandlers({ vpcSubnetDeleteV1: NotImplemented, vpcSubnetListV1: NotImplemented, vpcSubnetUpdateV1: NotImplemented, - vpcUpdateV1: NotImplemented, // deprecated by ID endpoints @@ -1131,5 +1126,10 @@ export const handlers = makeHandlers({ snapshotDelete: NotImplemented, snapshotList: NotImplemented, snapshotView: NotImplemented, + vpcCreate: NotImplemented, + vpcDelete: NotImplemented, + vpcList: NotImplemented, vpcSubnetView: NotImplemented, + vpcUpdate: NotImplemented, + vpcView: NotImplemented, }) From 1024c0bf3ba4be236ce62a24d615421b98516de0 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 11:31:45 -0600 Subject: [PATCH 23/39] network interface create --- app/forms/disk-attach.tsx | 1 + app/forms/network-interface-create.tsx | 10 ++++++---- libs/api-mocks/msw/handlers.ts | 15 +++++---------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/forms/disk-attach.tsx b/app/forms/disk-attach.tsx index f05126e05..02f730fc9 100644 --- a/app/forms/disk-attach.tsx +++ b/app/forms/disk-attach.tsx @@ -27,6 +27,7 @@ export function AttachDiskSideModalForm({ const { instanceName } = useParams() const projectSelector = useProjectSelector() + // TODO: pass in this mutation from outside so we don't have to do the instanceName check const attachDisk = useApiMutation('instanceDiskAttachV1', { onSuccess(data) { invariant(instanceName, 'instanceName is required') diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index e743773de..667b70225 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react' +import { useParams } from 'react-router-dom' import invariant from 'tiny-invariant' import type { NetworkInterfaceCreate } from '@oxide/api' @@ -13,7 +14,7 @@ import { SubnetListbox, TextField, } from 'app/components/form' -import { useAllParams, useProjectSelector } from 'app/hooks' +import { useProjectSelector } from 'app/hooks' const defaultValues: NetworkInterfaceCreate = { name: '', @@ -33,10 +34,11 @@ export default function CreateNetworkInterfaceForm({ onDismiss, }: CreateNetworkInterfaceFormProps) { const queryClient = useApiQueryClient() - const { orgName, projectName, instanceName } = useAllParams('orgName', 'projectName') + const { instanceName } = useParams() const projectSelector = useProjectSelector() - const createNetworkInterface = useApiMutation('instanceNetworkInterfaceCreate', { + // TODO: pass in this mutation from outside so we don't have to do the instanceName check + const createNetworkInterface = useApiMutation('instanceNetworkInterfaceCreateV1', { onSuccess() { invariant(instanceName, 'instanceName is required when posting a network interface') queryClient.invalidateQueries('instanceNetworkInterfaceListV1', { @@ -64,7 +66,7 @@ export default function CreateNetworkInterfaceForm({ ) createNetworkInterface.mutate({ - path: { instanceName, projectName, orgName }, + query: { ...projectSelector, instance: instanceName }, body, }) }) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index a79b7edad..0040567c7 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -393,8 +393,8 @@ export const handlers = makeHandlers({ const nics = db.networkInterfaces.filter((n) => n.instance_id === instance.id) return paginated(query, nics) }, - instanceNetworkInterfaceCreate({ body, ...params }) { - const instance = lookupInstance(params.path) + instanceNetworkInterfaceCreateV1({ body, query }) { + const instance = lookup.instance(query) const nicsForInstance = db.networkInterfaces.filter( (n) => n.instance_id === instance.id ) @@ -402,13 +402,8 @@ export const handlers = makeHandlers({ const { name, description, subnet_name, vpc_name, ip } = body - const vpc = lookupVpc({ ...params.path, vpcName: vpc_name }) - - const subnet = lookupVpcSubnet({ - ...params.path, - vpcName: vpc_name, - subnetName: subnet_name, - }) + const vpc = lookup.vpc({ ...query, vpc: vpc_name }) + const subnet = lookup.vpcSubnet({ ...query, vpc: vpc_name, subnet: subnet_name }) const newNic: Json = { id: uuid(), @@ -1043,7 +1038,6 @@ export const handlers = makeHandlers({ certificateViewV1: NotImplemented, instanceDeleteV1: NotImplemented, instanceMigrateV1: NotImplemented, - instanceNetworkInterfaceCreateV1: NotImplemented, instanceRebootV1: NotImplemented, instanceSerialConsoleStreamV1: NotImplemented, instanceSerialConsoleV1: NotImplemented, @@ -1109,6 +1103,7 @@ export const handlers = makeHandlers({ instanceDiskDetach: NotImplemented, instanceDiskList: NotImplemented, instanceList: NotImplemented, + instanceNetworkInterfaceCreate: NotImplemented, instanceNetworkInterfaceDelete: NotImplemented, instanceNetworkInterfaceList: NotImplemented, instanceNetworkInterfaceUpdate: NotImplemented, From 9f9727706a6167b22aab53fb7a2f14c88406f403 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 11:38:57 -0600 Subject: [PATCH 24/39] vpc subnets --- app/components/form/fields/SubnetListbox.tsx | 8 +++--- app/forms/subnet-create.tsx | 10 +++---- app/forms/subnet-edit.tsx | 11 ++++---- .../networking/VpcPage/tabs/VpcSubnetsTab.tsx | 6 ++--- libs/api-mocks/msw/handlers.ts | 26 +++++++++---------- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/app/components/form/fields/SubnetListbox.tsx b/app/components/form/fields/SubnetListbox.tsx index 5cc122727..f4359a572 100644 --- a/app/components/form/fields/SubnetListbox.tsx +++ b/app/components/form/fields/SubnetListbox.tsx @@ -3,7 +3,7 @@ import { useWatch } from 'react-hook-form' import { useApiQuery } from '@oxide/api' -import { useRequiredParams } from 'app/hooks' +import { useProjectSelector } from 'app/hooks' import type { ListboxFieldProps } from './ListboxField' import { ListboxField } from './ListboxField' @@ -26,7 +26,7 @@ export function SubnetListbox< TFieldValues extends FieldValues, TName extends FieldPath >({ vpcNameField, control, ...fieldProps }: SubnetListboxProps) { - const pathParams = useRequiredParams('orgName', 'projectName') + const projectSelector = useProjectSelector() const [vpcName] = useWatch({ control, name: [vpcNameField] }) @@ -36,8 +36,8 @@ export function SubnetListbox< // TODO: error handling other than fallback to empty list? const subnets = useApiQuery( - 'vpcSubnetList', - { path: { ...pathParams, vpcName } }, + 'vpcSubnetListV1', + { query: { ...projectSelector, vpc: vpcName } }, { enabled: vpcExists, useErrorBoundary: false, diff --git a/app/forms/subnet-create.tsx b/app/forms/subnet-create.tsx index 7a9163d49..a18bd06dc 100644 --- a/app/forms/subnet-create.tsx +++ b/app/forms/subnet-create.tsx @@ -3,7 +3,7 @@ import { useApiMutation, useApiQueryClient } from '@oxide/api' import { Divider } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm, TextField } from 'app/components/form' -import { useRequiredParams } from 'app/hooks' +import { useVpcSelector } from 'app/hooks' const defaultValues: VpcSubnetCreate = { name: '', @@ -16,12 +16,12 @@ type CreateSubnetFormProps = { } export function CreateSubnetForm({ onDismiss }: CreateSubnetFormProps) { - const parentNames = useRequiredParams('orgName', 'projectName', 'vpcName') + const vpcSelector = useVpcSelector() const queryClient = useApiQueryClient() - const createSubnet = useApiMutation('vpcSubnetCreate', { + const createSubnet = useApiMutation('vpcSubnetCreateV1', { onSuccess() { - queryClient.invalidateQueries('vpcSubnetList', { path: parentNames }) + queryClient.invalidateQueries('vpcSubnetListV1', { query: vpcSelector }) onDismiss() }, }) @@ -31,7 +31,7 @@ export function CreateSubnetForm({ onDismiss }: CreateSubnetFormProps) { title="Create subnet" formOptions={{ defaultValues }} onDismiss={onDismiss} - onSubmit={(body) => createSubnet.mutate({ path: parentNames, body })} + onSubmit={(body) => createSubnet.mutate({ query: vpcSelector, body })} loading={createSubnet.isLoading} submitError={createSubnet.error} > diff --git a/app/forms/subnet-edit.tsx b/app/forms/subnet-edit.tsx index 2196c4f69..a23f1af8f 100644 --- a/app/forms/subnet-edit.tsx +++ b/app/forms/subnet-edit.tsx @@ -3,7 +3,7 @@ import { useApiMutation, useApiQueryClient } from '@oxide/api' import { pick } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { useRequiredParams } from 'app/hooks' +import { useVpcSelector } from 'app/hooks' type EditSubnetFormProps = { onDismiss: () => void @@ -11,12 +11,12 @@ type EditSubnetFormProps = { } export function EditSubnetForm({ onDismiss, editing }: EditSubnetFormProps) { - const parentNames = useRequiredParams('orgName', 'projectName', 'vpcName') + const vpcSelector = useVpcSelector() const queryClient = useApiQueryClient() - const updateSubnet = useApiMutation('vpcSubnetUpdate', { + const updateSubnet = useApiMutation('vpcSubnetUpdateV1', { onSuccess() { - queryClient.invalidateQueries('vpcSubnetList', { path: parentNames }) + queryClient.invalidateQueries('vpcSubnetListV1', { query: vpcSelector }) onDismiss() }, }) @@ -31,7 +31,8 @@ export function EditSubnetForm({ onDismiss, editing }: EditSubnetFormProps) { formOptions={{ defaultValues }} onSubmit={(body) => { updateSubnet.mutate({ - path: { ...parentNames, subnetName: editing.name }, + path: { subnet: editing.name }, + query: vpcSelector, body, }) }} diff --git a/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx index fec0f34f0..b52ca1dea 100644 --- a/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx +++ b/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx @@ -7,12 +7,12 @@ import { Button, EmptyMessage } from '@oxide/ui' import { CreateSubnetForm } from 'app/forms/subnet-create' import { EditSubnetForm } from 'app/forms/subnet-edit' -import { useRequiredParams } from 'app/hooks' +import { useVpcSelector } from 'app/hooks' export const VpcSubnetsTab = () => { - const vpcParams = useRequiredParams('orgName', 'projectName', 'vpcName') + const vpcSelector = useVpcSelector() - const { Table, Column } = useQueryTable('vpcSubnetList', { path: vpcParams }) + const { Table, Column } = useQueryTable('vpcSubnetListV1', { query: vpcSelector }) const [creating, setCreating] = useState(false) const [editing, setEditing] = useState(null) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 0040567c7..b7c72b579 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -710,13 +710,13 @@ export const handlers = makeHandlers({ db.vpcRouterRoutes = db.vpcRouterRoutes.filter((r) => r.id !== route.id) return 204 }, - vpcSubnetList(params) { - const vpc = lookupVpc(params.path) + vpcSubnetListV1({ query }) { + const vpc = lookup.vpc(query) const subnets = db.vpcSubnets.filter((s) => s.vpc_id === vpc.id) - return paginated(params.query, subnets) + return paginated(query, subnets) }, - vpcSubnetCreate({ body, ...params }) { - const vpc = lookupVpc(params.path) + vpcSubnetCreateV1({ body, query }) { + const vpc = lookup.vpc(query) errIfExists(db.vpcSubnets, { vpc_id: vpc.id, name: body.name }) // TODO: Create a route for the subnet in the default router @@ -734,8 +734,8 @@ export const handlers = makeHandlers({ return json(newSubnet, { status: 201 }) }, vpcSubnetViewV1: ({ path, query }) => lookup.vpcSubnet({ ...path, ...query }), - vpcSubnetUpdate({ body, ...params }) { - const subnet = lookupVpcSubnet(params.path) + vpcSubnetUpdateV1({ body, path, query }) { + const subnet = lookup.vpcSubnet({ ...path, ...query }) if (body.name) { subnet.name = body.name @@ -746,8 +746,8 @@ export const handlers = makeHandlers({ return subnet }, - vpcSubnetDelete(params) { - const subnet = lookupVpcSubnet(params.path) + vpcSubnetDeleteV1({ path, query }) { + const subnet = lookup.vpcSubnet({ ...path, ...query }) db.vpcSubnets = db.vpcSubnets.filter((s) => s.id !== subnet.id) return 204 @@ -1071,10 +1071,10 @@ export const handlers = makeHandlers({ vpcRouterRouteViewV1: NotImplemented, vpcRouterUpdateV1: NotImplemented, vpcRouterViewV1: NotImplemented, - vpcSubnetCreateV1: NotImplemented, - vpcSubnetDeleteV1: NotImplemented, - vpcSubnetListV1: NotImplemented, - vpcSubnetUpdateV1: NotImplemented, + vpcSubnetCreate: NotImplemented, + vpcSubnetDelete: NotImplemented, + vpcSubnetList: NotImplemented, + vpcSubnetUpdate: NotImplemented, // deprecated by ID endpoints From ff5ab794fd740fc09a13a6b5821f08d3b6b69d64 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 11:52:35 -0600 Subject: [PATCH 25/39] convert instance actions and delete some unused looker-uppers --- app/pages/project/instances/InstancesPage.tsx | 10 ++---- app/pages/project/instances/actions.tsx | 22 ++++++------- .../instances/instance/InstancePage.tsx | 2 +- libs/api-mocks/msw/db.ts | 22 ------------- libs/api-mocks/msw/handlers.ts | 31 +++++++++---------- 5 files changed, 30 insertions(+), 57 deletions(-) diff --git a/app/pages/project/instances/InstancesPage.tsx b/app/pages/project/instances/InstancesPage.tsx index 8743c653f..e988a57ad 100644 --- a/app/pages/project/instances/InstancesPage.tsx +++ b/app/pages/project/instances/InstancesPage.tsx @@ -45,18 +45,14 @@ InstancesPage.loader = async ({ params }: LoaderFunctionArgs) => { export function InstancesPage() { const projectSelector = useProjectSelector() - const { organization, project } = projectSelector const queryClient = useApiQueryClient() const refetchInstances = () => queryClient.invalidateQueries('instanceListV1', { query: projectSelector }) - const makeActions = useMakeInstanceActions( - { orgName: organization, projectName: project }, - { - onSuccess: refetchInstances, - } - ) + const makeActions = useMakeInstanceActions(projectSelector, { + onSuccess: refetchInstances, + }) const { data: instances } = useApiQuery('instanceListV1', { query: { ...projectSelector, limit: 10 }, // to have same params as QueryTable diff --git a/app/pages/project/instances/actions.tsx b/app/pages/project/instances/actions.tsx index a8f48610e..7167b6b8a 100644 --- a/app/pages/project/instances/actions.tsx +++ b/app/pages/project/instances/actions.tsx @@ -1,13 +1,12 @@ import { useCallback } from 'react' import { useNavigate } from 'react-router-dom' -import type { Instance } from '@oxide/api' -import { useApiMutation } from '@oxide/api' +import { type Instance, toPathQuery, useApiMutation } from '@oxide/api' import type { MakeActions } from '@oxide/table' import { Success16Icon } from '@oxide/ui' import { useToast } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' const instanceCan: Record boolean> = { start: (i) => i.runState === 'stopped', @@ -26,7 +25,7 @@ type Options = { } export const useMakeInstanceActions = ( - projectParams: { orgName: string; projectName: string }, + projectSelector: { organization: string; project: string }, options: Options = {} ): MakeActions => { const navigate = useNavigate() @@ -36,14 +35,15 @@ export const useMakeInstanceActions = ( // if you also pass onSuccess to mutate(), this one is not overridden — this // one runs first, then the one passed to mutate() const opts = { onSuccess: options.onSuccess } - const startInstance = useApiMutation('instanceStart', opts) - const stopInstance = useApiMutation('instanceStop', opts) - const rebootInstance = useApiMutation('instanceReboot', opts) - const deleteInstance = useApiMutation('instanceDelete', opts) + const startInstance = useApiMutation('instanceStartV1', opts) + const stopInstance = useApiMutation('instanceStopV1', opts) + const rebootInstance = useApiMutation('instanceRebootV1', opts) + const deleteInstance = useApiMutation('instanceDeleteV1', opts) return useCallback((instance) => { - const { name: instanceName } = instance - const instanceParams = { path: { ...projectParams, instanceName } } + const instanceName = instance.name + const instanceSelector = { ...projectSelector, instance: instanceName } + const instanceParams = toPathQuery('instance', instanceSelector) return [ { label: 'Start', @@ -75,7 +75,7 @@ export const useMakeInstanceActions = ( { label: 'View serial console', onActivate() { - navigate(pb.serialConsole(instanceParams.path)) + navigate(pb2.serialConsole(instanceSelector)) }, }, { diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index 01179a640..c045847c4 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -31,7 +31,7 @@ export function InstancePage() { const queryClient = useApiQueryClient() // TODO: change the interface here to take projectSelector directly const makeActions = useMakeInstanceActions( - { projectName: project, orgName: organization }, + { project, organization }, { onSuccess: () => { queryClient.invalidateQueries('instanceViewV1', instancePathQuery) diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 6f3cf468f..f189b14d4 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -148,17 +148,6 @@ export function lookupVpc(params: PP.Vpc): Json { return vpc } -export function lookupInstance(params: PP.Instance): Json { - const project = lookupProject(params) - - const instance = db.instances.find( - (p) => p.project_id === project.id && p.name === params.instanceName - ) - if (!instance) throw notFoundErr - - return instance -} - export function lookupImage(params: PP.Image): Json { const project = lookupProject(params) @@ -170,17 +159,6 @@ export function lookupImage(params: PP.Image): Json { return image } -export function lookupVpcSubnet(params: PP.VpcSubnet): Json { - const vpc = lookupVpc(params) - - const subnet = db.vpcSubnets.find( - (p) => p.vpc_id === vpc.id && p.name === params.subnetName - ) - if (!subnet) throw notFoundErr - - return subnet -} - export function lookupVpcRouter(params: PP.VpcRouter): Json { const vpc = lookupVpc(params) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index b7c72b579..1c495296c 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -1,6 +1,7 @@ import { v4 as uuid } from 'uuid' import type { ApiTypes as Api, UpdateDeployment } from '@oxide/api' +import { toApiSelector } from '@oxide/api' import type { Json } from '@oxide/gen/msw-handlers' import { json, makeHandlers } from '@oxide/gen/msw-handlers' import { pick, sortBy } from '@oxide/util' @@ -17,7 +18,6 @@ import { lookupById, lookupGlobalImage, lookupImage, - lookupInstance, lookupOrg, lookupProject, lookupSamlIdp, @@ -27,7 +27,6 @@ import { lookupVpc, lookupVpcRouter, lookupVpcRouterRoute, - lookupVpcSubnet, } from './db' import { NotImplemented, @@ -340,8 +339,8 @@ export const handlers = makeHandlers({ return json(newInstance, { status: 201 }) }, instanceViewV1: ({ path, query }) => lookup.instance({ ...path, ...query }), - instanceDelete(params) { - const instance = lookupInstance(params.path) + instanceDeleteV1({ path, query }) { + const instance = lookup.instance({ ...path, ...query }) db.instances = db.instances.filter((i) => i.id !== instance.id) return 204 }, @@ -376,7 +375,7 @@ export const handlers = makeHandlers({ return disk }, instanceExternalIpList(params) { - lookupInstance(params.path) + lookup.instance(toApiSelector(params.path)) // temporary // TODO: proper mock table return { @@ -453,8 +452,8 @@ export const handlers = makeHandlers({ db.networkInterfaces = db.networkInterfaces.filter((n) => n.id !== nic.id) return 204 }, - instanceReboot(params) { - const instance = lookupInstance(params.path) + instanceRebootV1({ path, query }) { + const instance = lookup.instance({ ...path, ...query }) instance.run_state = 'rebooting' setTimeout(() => { @@ -467,14 +466,14 @@ export const handlers = makeHandlers({ // TODO: Add support for params return serial }, - instanceStart(params) { - const instance = lookupInstance(params.path) + instanceStartV1({ path, query }) { + const instance = lookup.instance({ ...path, ...query }) instance.run_state = 'running' return json(instance, { status: 202 }) }, - instanceStop(params) { - const instance = lookupInstance(params.path) + instanceStopV1({ path, query }) { + const instance = lookup.instance({ ...path, ...query }) instance.run_state = 'stopped' return json(instance, { status: 202 }) @@ -753,7 +752,7 @@ export const handlers = makeHandlers({ return 204 }, vpcSubnetListNetworkInterfaces(params) { - const subnet = lookupVpcSubnet(params.path) + const subnet = lookup.vpcSubnet(toApiSelector(params.path)) const nics = db.networkInterfaces.filter((n) => n.subnet_id === subnet.id) return paginated(params.query, nics) }, @@ -1036,13 +1035,9 @@ export const handlers = makeHandlers({ certificateDeleteV1: NotImplemented, certificateListV1: NotImplemented, certificateViewV1: NotImplemented, - instanceDeleteV1: NotImplemented, instanceMigrateV1: NotImplemented, - instanceRebootV1: NotImplemented, instanceSerialConsoleStreamV1: NotImplemented, instanceSerialConsoleV1: NotImplemented, - instanceStartV1: NotImplemented, - instanceStopV1: NotImplemented, organizationDeleteV1: NotImplemented, organizationPolicyUpdateV1: NotImplemented, organizationPolicyViewV1: NotImplemented, @@ -1099,6 +1094,7 @@ export const handlers = makeHandlers({ diskList: NotImplemented, diskView: NotImplemented, instanceCreate: NotImplemented, + instanceDelete: NotImplemented, instanceDiskAttach: NotImplemented, instanceDiskDetach: NotImplemented, instanceDiskList: NotImplemented, @@ -1108,6 +1104,9 @@ export const handlers = makeHandlers({ instanceNetworkInterfaceList: NotImplemented, instanceNetworkInterfaceUpdate: NotImplemented, instanceNetworkInterfaceView: NotImplemented, + instanceReboot: NotImplemented, + instanceStart: NotImplemented, + instanceStop: NotImplemented, instanceView: NotImplemented, organizationCreate: NotImplemented, organizationList: NotImplemented, From 92302a9c120a0a0bb7395b7dc98e6fef2efc462f Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 12:04:46 -0600 Subject: [PATCH 26/39] remaining org endpoints --- app/forms/org-access.tsx | 18 ++++++------- app/forms/org-edit.tsx | 12 ++++----- app/pages/OrgAccessPage.tsx | 20 ++++++++------- app/pages/OrgsPage.tsx | 21 +++++++++------- .../project/access/ProjectAccessPage.tsx | 17 ++++++++++--- libs/api-mocks/msw/handlers.ts | 25 +++++++++---------- 6 files changed, 62 insertions(+), 51 deletions(-) diff --git a/app/forms/org-access.tsx b/app/forms/org-access.tsx index be253aa91..ec3d77c50 100644 --- a/app/forms/org-access.tsx +++ b/app/forms/org-access.tsx @@ -6,20 +6,20 @@ import { } from '@oxide/api' import { ListboxField, SideModalForm } from 'app/components/form' -import { useRequiredParams } from 'app/hooks' +import { useOrgSelector } from 'app/hooks' import type { AddRoleModalProps, EditRoleModalProps } from './access-util' import { actorToItem, defaultValues, roleItems } from './access-util' export function OrgAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalProps) { - const orgParams = useRequiredParams('orgName') + const { organization } = useOrgSelector() const actors = useActorsNotInPolicy(policy) const queryClient = useApiQueryClient() - const updatePolicy = useApiMutation('organizationPolicyUpdate', { + const updatePolicy = useApiMutation('organizationPolicyUpdateV1', { onSuccess: () => { - queryClient.invalidateQueries('organizationPolicyView', { path: orgParams }) + queryClient.invalidateQueries('organizationPolicyViewV1', { path: { organization } }) onDismiss() }, }) @@ -39,7 +39,7 @@ export function OrgAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalPro const identityType = actors.find((a) => a.id === identityId)!.identityType updatePolicy.mutate({ - path: orgParams, + path: { organization }, body: updateRole({ identityId, identityType, roleName }, policy), }) }} @@ -76,12 +76,12 @@ export function OrgAccessEditUserSideModal({ policy, defaultValues, }: EditRoleModalProps) { - const orgParams = useRequiredParams('orgName') + const { organization } = useOrgSelector() const queryClient = useApiQueryClient() - const updatePolicy = useApiMutation('organizationPolicyUpdate', { + const updatePolicy = useApiMutation('organizationPolicyUpdateV1', { onSuccess: () => { - queryClient.invalidateQueries('organizationPolicyView', { path: orgParams }) + queryClient.invalidateQueries('organizationPolicyViewV1', { path: { organization } }) onDismiss() }, }) @@ -94,7 +94,7 @@ export function OrgAccessEditUserSideModal({ formOptions={{ defaultValues }} onSubmit={({ roleName }) => { updatePolicy.mutate({ - path: orgParams, + path: { organization }, body: updateRole({ identityId, identityType, roleName }, policy), }) }} diff --git a/app/forms/org-edit.tsx b/app/forms/org-edit.tsx index 0ec70b4a9..d2a8a6019 100644 --- a/app/forms/org-edit.tsx +++ b/app/forms/org-edit.tsx @@ -5,7 +5,7 @@ import { apiQueryClient, useApiMutation, useApiQuery, useApiQueryClient } from ' import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { requireOrgParams, useOrgParams, useToast } from 'app/hooks' +import { requireOrgParams, useOrgSelector, useToast } from 'app/hooks' import { pb } from 'app/util/path-builder' EditOrgSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { @@ -21,15 +21,13 @@ export function EditOrgSideModalForm() { const addToast = useToast() const navigate = useNavigate() - const { orgName } = useOrgParams() + const { organization } = useOrgSelector() const onDismiss = () => navigate(pb.orgs()) - const { data: org } = useApiQuery('organizationViewV1', { - path: { organization: orgName }, - }) + const { data: org } = useApiQuery('organizationViewV1', { path: { organization } }) - const updateOrg = useApiMutation('organizationUpdate', { + const updateOrg = useApiMutation('organizationUpdateV1', { onSuccess(org) { queryClient.invalidateQueries('organizationListV1', {}) // avoid the org fetch when the org page loads since we have the data @@ -56,7 +54,7 @@ export function EditOrgSideModalForm() { onDismiss={onDismiss} onSubmit={({ name, description }) => updateOrg.mutate({ - path: { orgName }, + path: { organization }, body: { name, description }, }) } diff --git a/app/pages/OrgAccessPage.tsx b/app/pages/OrgAccessPage.tsx index ba864d643..66f43208a 100644 --- a/app/pages/OrgAccessPage.tsx +++ b/app/pages/OrgAccessPage.tsx @@ -27,7 +27,7 @@ import { groupBy, isTruthy } from '@oxide/util' import { AccessNameCell } from 'app/components/AccessNameCell' import { RoleBadgeCell } from 'app/components/RoleBadgeCell' import { OrgAccessAddUserSideModal, OrgAccessEditUserSideModal } from 'app/forms/org-access' -import { requireOrgParams, useRequiredParams } from 'app/hooks' +import { getProjectSelector, useOrgSelector } from 'app/hooks' const EmptyState = ({ onClick }: { onClick: () => void }) => ( @@ -44,8 +44,8 @@ const EmptyState = ({ onClick }: { onClick: () => void }) => ( OrgAccessPage.loader = async ({ params }: LoaderFunctionArgs) => { await Promise.all([ apiQueryClient.prefetchQuery('policyView', {}), - apiQueryClient.prefetchQuery('organizationPolicyView', { - path: requireOrgParams(params), + apiQueryClient.prefetchQuery('organizationPolicyViewV1', { + path: getProjectSelector(params), }), // used to resolve user names apiQueryClient.prefetchQuery('userList', {}), @@ -69,12 +69,14 @@ const colHelper = createColumnHelper() export function OrgAccessPage() { const [addModalOpen, setAddModalOpen] = useState(false) const [editingUserRow, setEditingUserRow] = useState(null) - const orgParams = useRequiredParams('orgName') + const { organization } = useOrgSelector() const { data: siloPolicy } = useApiQuery('policyView', {}) const siloRows = useUserRows(siloPolicy?.roleAssignments, 'silo') - const { data: orgPolicy } = useApiQuery('organizationPolicyView', { path: orgParams }) + const { data: orgPolicy } = useApiQuery('organizationPolicyViewV1', { + path: { organization }, + }) const orgRows = useUserRows(orgPolicy?.roleAssignments, 'org') const rows = useMemo(() => { @@ -103,9 +105,9 @@ export function OrgAccessPage() { }, [siloRows, orgRows]) const queryClient = useApiQueryClient() - const updatePolicy = useApiMutation('organizationPolicyUpdate', { + const updatePolicy = useApiMutation('organizationPolicyUpdateV1', { onSuccess: () => - queryClient.invalidateQueries('organizationPolicyView', { path: orgParams }), + queryClient.invalidateQueries('organizationPolicyViewV1', { path: { organization } }), // TODO: handle 403 }) @@ -137,7 +139,7 @@ export function OrgAccessPage() { onActivate() { // TODO: confirm delete updatePolicy.mutate({ - path: orgParams, + path: { organization }, // we know policy is there, otherwise there's no row to display body: deleteRole(row.id, orgPolicy!), }) @@ -146,7 +148,7 @@ export function OrgAccessPage() { }, ]), ], - [orgPolicy, orgParams, updatePolicy] + [orgPolicy, organization, updatePolicy] ) const tableInstance = useReactTable({ columns, data: rows }) diff --git a/app/pages/OrgsPage.tsx b/app/pages/OrgsPage.tsx index 3c0e3751b..7f34b604b 100644 --- a/app/pages/OrgsPage.tsx +++ b/app/pages/OrgsPage.tsx @@ -16,7 +16,7 @@ import { buttonStyle, } from '@oxide/ui' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' import { useQuickActions } from '../hooks' @@ -26,7 +26,7 @@ const EmptyState = () => ( title="No organizations" body="You need to create an organization to be able to see it here" buttonText="New organization" - buttonTo={pb.orgNew()} + buttonTo={pb2.orgNew()} /> ) @@ -45,7 +45,7 @@ export default function OrgsPage() { query: { limit: 10 }, // to have same params as QueryTable }) - const deleteOrg = useApiMutation('organizationDelete', { + const deleteOrg = useApiMutation('organizationDeleteV1', { onSuccess() { queryClient.invalidateQueries('organizationListV1', {}) }, @@ -60,13 +60,13 @@ export default function OrgsPage() { { path: { organization: org.name } }, org ) - navigate(pb.orgEdit({ orgName: org.name })) + navigate(pb2.orgEdit({ organization: org.name })) }, }, { label: 'Delete', onActivate: () => { - deleteOrg.mutate({ path: { orgName: org.name } }) + deleteOrg.mutate({ path: { organization: org.name } }) }, }, ] @@ -74,10 +74,10 @@ export default function OrgsPage() { useQuickActions( useMemo( () => [ - { value: 'New organization', onSelect: () => navigate(pb.orgNew()) }, + { value: 'New organization', onSelect: () => navigate(pb2.orgNew()) }, ...(orgs?.items || []).map((o) => ({ value: o.name, - onSelect: () => navigate(pb.org({ orgName: o.name })), + onSelect: () => navigate(pb2.org({ organization: o.name })), navGroup: 'Go to organization', })), ], @@ -91,12 +91,15 @@ export default function OrgsPage() { }>Organizations - + New Organization
} makeActions={makeActions}> - pb.projects({ orgName }))} /> + pb2.projects({ organization }))} + />
diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 715bf81fc..eed84dce5 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -31,7 +31,12 @@ import { ProjectAccessAddUserSideModal, ProjectAccessEditUserSideModal, } from 'app/forms/project-access' -import { requireProjectParams, useRequiredParams } from 'app/hooks' +import { + getProjectSelector, + requireProjectParams, + useProjectSelector, + useRequiredParams, +} from 'app/hooks' const EmptyState = ({ onClick }: { onClick: () => void }) => ( @@ -47,9 +52,10 @@ const EmptyState = ({ onClick }: { onClick: () => void }) => ( ProjectAccessPage.loader = async ({ params }: LoaderFunctionArgs) => { const { orgName, projectName } = requireProjectParams(params) + const { organization } = getProjectSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('policyView', {}), - apiQueryClient.prefetchQuery('organizationPolicyView', { path: { orgName } }), + apiQueryClient.prefetchQuery('organizationPolicyViewV1', { path: { organization } }), apiQueryClient.prefetchQuery('projectPolicyView', { path: { orgName, projectName } }), // used to resolve user names apiQueryClient.prefetchQuery('userList', {}), @@ -73,13 +79,16 @@ const colHelper = createColumnHelper() export function ProjectAccessPage() { const [addModalOpen, setAddModalOpen] = useState(false) const [editingUserRow, setEditingUserRow] = useState(null) + const projectSelector = useProjectSelector() + const { organization } = projectSelector const projectParams = useRequiredParams('orgName', 'projectName') - const { orgName } = projectParams const { data: siloPolicy } = useApiQuery('policyView', {}) const siloRows = useUserRows(siloPolicy?.roleAssignments, 'silo') - const { data: orgPolicy } = useApiQuery('organizationPolicyView', { path: { orgName } }) + const { data: orgPolicy } = useApiQuery('organizationPolicyViewV1', { + path: { organization }, + }) const orgRows = useUserRows(orgPolicy?.roleAssignments, 'org') const { data: projectPolicy } = useApiQuery('projectPolicyView', { path: projectParams }) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 1c495296c..e302e55bf 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -18,7 +18,6 @@ import { lookupById, lookupGlobalImage, lookupImage, - lookupOrg, lookupProject, lookupSamlIdp, lookupSilo, @@ -69,8 +68,8 @@ export const handlers = makeHandlers({ return lookup.org(params.path) }, - organizationUpdate({ body, ...params }) { - const org = lookupOrg(params.path) + organizationUpdateV1({ body, path }) { + const org = lookup.org(path) if (typeof body.name === 'string') { org.name = body.name @@ -79,13 +78,13 @@ export const handlers = makeHandlers({ return org }, - organizationDelete(params) { - const org = lookupOrg(params.path) + organizationDeleteV1({ path }) { + const org = lookup.org(path) db.orgs = db.orgs.filter((o) => o.id !== org.id) return 204 }, - organizationPolicyView(params) { - const org = lookupOrg(params.path) + organizationPolicyViewV1({ path }) { + const org = lookup.org(path) const role_assignments = db.roleAssignments .filter((r) => r.resource_type === 'organization' && r.resource_id === org.id) @@ -93,8 +92,8 @@ export const handlers = makeHandlers({ return { role_assignments } }, - organizationPolicyUpdate({ body, ...params }) { - const org = lookupOrg(params.path) + organizationPolicyUpdateV1({ body, path }) { + const org = lookup.org(path) const newAssignments = body.role_assignments.map((r) => ({ resource_type: 'organization' as const, @@ -1038,10 +1037,6 @@ export const handlers = makeHandlers({ instanceMigrateV1: NotImplemented, instanceSerialConsoleStreamV1: NotImplemented, instanceSerialConsoleV1: NotImplemented, - organizationDeleteV1: NotImplemented, - organizationPolicyUpdateV1: NotImplemented, - organizationPolicyViewV1: NotImplemented, - organizationUpdateV1: NotImplemented, physicalDiskListV1: NotImplemented, policyUpdateV1: NotImplemented, policyViewV1: NotImplemented, @@ -1109,7 +1104,11 @@ export const handlers = makeHandlers({ instanceStop: NotImplemented, instanceView: NotImplemented, organizationCreate: NotImplemented, + organizationDelete: NotImplemented, organizationList: NotImplemented, + organizationPolicyUpdate: NotImplemented, + organizationPolicyView: NotImplemented, + organizationUpdate: NotImplemented, organizationView: NotImplemented, projectCreate: NotImplemented, projectDelete: NotImplemented, From 83360efbc3fe631fddb0c8194430ce2703010e3a Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 12:17:05 -0600 Subject: [PATCH 27/39] silo and project policy --- app/forms/project-access.tsx | 19 +++++----- app/forms/silo-access.tsx | 8 ++--- app/pages/OrgAccessPage.tsx | 4 +-- app/pages/SiloAccessPage.tsx | 8 ++--- .../project/access/ProjectAccessPage.tsx | 35 +++++++++---------- libs/api-mocks/msw/handlers.ts | 25 +++++++------ 6 files changed, 48 insertions(+), 51 deletions(-) diff --git a/app/forms/project-access.tsx b/app/forms/project-access.tsx index 037c925d2..fcd61ad32 100644 --- a/app/forms/project-access.tsx +++ b/app/forms/project-access.tsx @@ -1,4 +1,5 @@ import { + toPathQuery, updateRole, useActorsNotInPolicy, useApiMutation, @@ -6,20 +7,20 @@ import { } from '@oxide/api' import { ListboxField, SideModalForm } from 'app/components/form' -import { useRequiredParams } from 'app/hooks' +import { useProjectSelector } from 'app/hooks' import type { AddRoleModalProps, EditRoleModalProps } from './access-util' import { actorToItem, defaultValues, roleItems } from './access-util' export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalProps) { - const projectParams = useRequiredParams('orgName', 'projectName') + const projectPathQuery = toPathQuery('project', useProjectSelector()) const actors = useActorsNotInPolicy(policy) const queryClient = useApiQueryClient() - const updatePolicy = useApiMutation('projectPolicyUpdate', { + const updatePolicy = useApiMutation('projectPolicyUpdateV1', { onSuccess: () => { - queryClient.invalidateQueries('projectPolicyView', { path: projectParams }) + queryClient.invalidateQueries('projectPolicyViewV1', projectPathQuery) onDismiss() }, }) @@ -38,7 +39,7 @@ export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModa const identityType = actors.find((a) => a.id === identityId)!.identityType updatePolicy.mutate({ - path: projectParams, + ...projectPathQuery, body: updateRole({ identityId, identityType, roleName }, policy), }) }} @@ -76,12 +77,12 @@ export function ProjectAccessEditUserSideModal({ policy, defaultValues, }: EditRoleModalProps) { - const projectParams = useRequiredParams('orgName', 'projectName') + const projectPathQuery = toPathQuery('project', useProjectSelector()) const queryClient = useApiQueryClient() - const updatePolicy = useApiMutation('projectPolicyUpdate', { + const updatePolicy = useApiMutation('projectPolicyUpdateV1', { onSuccess: () => { - queryClient.invalidateQueries('projectPolicyView', { path: projectParams }) + queryClient.invalidateQueries('projectPolicyViewV1', projectPathQuery) onDismiss() }, }) @@ -94,7 +95,7 @@ export function ProjectAccessEditUserSideModal({ formOptions={{ defaultValues }} onSubmit={({ roleName }) => { updatePolicy.mutate({ - path: projectParams, + ...projectPathQuery, body: updateRole({ identityId, identityType, roleName }, policy), }) }} diff --git a/app/forms/silo-access.tsx b/app/forms/silo-access.tsx index ca638a422..7135fb8af 100644 --- a/app/forms/silo-access.tsx +++ b/app/forms/silo-access.tsx @@ -14,9 +14,9 @@ export function SiloAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalPr const actors = useActorsNotInPolicy(policy) const queryClient = useApiQueryClient() - const updatePolicy = useApiMutation('policyUpdate', { + const updatePolicy = useApiMutation('policyUpdateV1', { onSuccess: () => { - queryClient.invalidateQueries('policyView', {}) + queryClient.invalidateQueries('policyViewV1', {}) onDismiss() }, }) @@ -74,9 +74,9 @@ export function SiloAccessEditUserSideModal({ defaultValues, }: EditRoleModalProps) { const queryClient = useApiQueryClient() - const updatePolicy = useApiMutation('policyUpdate', { + const updatePolicy = useApiMutation('policyUpdateV1', { onSuccess: () => { - queryClient.invalidateQueries('policyView', {}) + queryClient.invalidateQueries('policyViewV1', {}) onDismiss() }, }) diff --git a/app/pages/OrgAccessPage.tsx b/app/pages/OrgAccessPage.tsx index 66f43208a..7a7022346 100644 --- a/app/pages/OrgAccessPage.tsx +++ b/app/pages/OrgAccessPage.tsx @@ -43,7 +43,7 @@ const EmptyState = ({ onClick }: { onClick: () => void }) => ( OrgAccessPage.loader = async ({ params }: LoaderFunctionArgs) => { await Promise.all([ - apiQueryClient.prefetchQuery('policyView', {}), + apiQueryClient.prefetchQuery('policyViewV1', {}), apiQueryClient.prefetchQuery('organizationPolicyViewV1', { path: getProjectSelector(params), }), @@ -71,7 +71,7 @@ export function OrgAccessPage() { const [editingUserRow, setEditingUserRow] = useState(null) const { organization } = useOrgSelector() - const { data: siloPolicy } = useApiQuery('policyView', {}) + const { data: siloPolicy } = useApiQuery('policyViewV1', {}) const siloRows = useUserRows(siloPolicy?.roleAssignments, 'silo') const { data: orgPolicy } = useApiQuery('organizationPolicyViewV1', { diff --git a/app/pages/SiloAccessPage.tsx b/app/pages/SiloAccessPage.tsx index 255d16c4d..fe42d7941 100644 --- a/app/pages/SiloAccessPage.tsx +++ b/app/pages/SiloAccessPage.tsx @@ -44,7 +44,7 @@ const EmptyState = ({ onClick }: { onClick: () => void }) => ( SiloAccessPage.loader = async () => { await Promise.all([ - apiQueryClient.prefetchQuery('policyView', {}), + apiQueryClient.prefetchQuery('policyViewV1', {}), // used to resolve user names apiQueryClient.prefetchQuery('userList', {}), apiQueryClient.prefetchQuery('groupList', {}), @@ -66,7 +66,7 @@ export function SiloAccessPage() { const [addModalOpen, setAddModalOpen] = useState(false) const [editingUserRow, setEditingUserRow] = useState(null) - const { data: siloPolicy } = useApiQuery('policyView', {}) + const { data: siloPolicy } = useApiQuery('policyViewV1', {}) const siloRows = useUserRows(siloPolicy?.roleAssignments, 'silo') const rows = useMemo(() => { @@ -93,8 +93,8 @@ export function SiloAccessPage() { }, [siloRows]) const queryClient = useApiQueryClient() - const updatePolicy = useApiMutation('policyUpdate', { - onSuccess: () => queryClient.invalidateQueries('policyView', {}), + const updatePolicy = useApiMutation('policyUpdateV1', { + onSuccess: () => queryClient.invalidateQueries('policyViewV1', {}), // TODO: handle 403 }) diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index eed84dce5..4745ff21b 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -3,10 +3,11 @@ import { useMemo, useState } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' import type { IdentityType, RoleKey } from '@oxide/api' -import { deleteRole } from '@oxide/api' +import { toPathQuery } from '@oxide/api' import { apiQueryClient, byGroupThenName, + deleteRole, getEffectiveRole, useApiMutation, useApiQuery, @@ -31,12 +32,7 @@ import { ProjectAccessAddUserSideModal, ProjectAccessEditUserSideModal, } from 'app/forms/project-access' -import { - getProjectSelector, - requireProjectParams, - useProjectSelector, - useRequiredParams, -} from 'app/hooks' +import { getProjectSelector, useProjectSelector } from 'app/hooks' const EmptyState = ({ onClick }: { onClick: () => void }) => ( @@ -51,12 +47,14 @@ const EmptyState = ({ onClick }: { onClick: () => void }) => ( ) ProjectAccessPage.loader = async ({ params }: LoaderFunctionArgs) => { - const { orgName, projectName } = requireProjectParams(params) - const { organization } = getProjectSelector(params) + const { organization, project } = getProjectSelector(params) await Promise.all([ - apiQueryClient.prefetchQuery('policyView', {}), + apiQueryClient.prefetchQuery('policyViewV1', {}), apiQueryClient.prefetchQuery('organizationPolicyViewV1', { path: { organization } }), - apiQueryClient.prefetchQuery('projectPolicyView', { path: { orgName, projectName } }), + apiQueryClient.prefetchQuery('projectPolicyViewV1', { + path: { project }, + query: { organization }, + }), // used to resolve user names apiQueryClient.prefetchQuery('userList', {}), apiQueryClient.prefetchQuery('groupList', {}), @@ -80,10 +78,10 @@ export function ProjectAccessPage() { const [addModalOpen, setAddModalOpen] = useState(false) const [editingUserRow, setEditingUserRow] = useState(null) const projectSelector = useProjectSelector() + const projectPathQuery = toPathQuery('project', projectSelector) const { organization } = projectSelector - const projectParams = useRequiredParams('orgName', 'projectName') - const { data: siloPolicy } = useApiQuery('policyView', {}) + const { data: siloPolicy } = useApiQuery('policyViewV1', {}) const siloRows = useUserRows(siloPolicy?.roleAssignments, 'silo') const { data: orgPolicy } = useApiQuery('organizationPolicyViewV1', { @@ -91,7 +89,7 @@ export function ProjectAccessPage() { }) const orgRows = useUserRows(orgPolicy?.roleAssignments, 'org') - const { data: projectPolicy } = useApiQuery('projectPolicyView', { path: projectParams }) + const { data: projectPolicy } = useApiQuery('projectPolicyViewV1', projectPathQuery) const projectRows = useUserRows(projectPolicy?.roleAssignments, 'project') const rows = useMemo(() => { @@ -124,9 +122,8 @@ export function ProjectAccessPage() { }, [siloRows, orgRows, projectRows]) const queryClient = useApiQueryClient() - const updatePolicy = useApiMutation('projectPolicyUpdate', { - onSuccess: () => - queryClient.invalidateQueries('projectPolicyView', { path: projectParams }), + const updatePolicy = useApiMutation('projectPolicyUpdateV1', { + onSuccess: () => queryClient.invalidateQueries('projectPolicyViewV1', projectPathQuery), // TODO: handle 403 }) @@ -163,7 +160,7 @@ export function ProjectAccessPage() { onActivate() { // TODO: confirm delete updatePolicy.mutate({ - path: projectParams, + ...projectPathQuery, // we know policy is there, otherwise there's no row to display body: deleteRole(row.id, projectPolicy!), }) @@ -172,7 +169,7 @@ export function ProjectAccessPage() { }, ]), ], - [projectPolicy, projectParams, updatePolicy] + [projectPolicy, projectPathQuery, updatePolicy] ) const tableInstance = useReactTable({ columns, data: rows }) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index e302e55bf..2eb55c10f 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -18,7 +18,6 @@ import { lookupById, lookupGlobalImage, lookupImage, - lookupProject, lookupSamlIdp, lookupSilo, lookupSled, @@ -210,12 +209,12 @@ export const handlers = makeHandlers({ } }, imageList(params) { - const project = lookupProject(params.path) + const project = lookup.project(toApiSelector(params.path)) const images = db.images.filter((i) => i.project_id === project.id) return paginated(params.query, images) }, imageCreate({ body, ...params }) { - const project = lookupProject(params.path) + const project = lookup.project(toApiSelector(params.path)) errIfExists(db.images, { name: body.name, project_id: project.id }) const newImage: Json = { @@ -477,8 +476,8 @@ export const handlers = makeHandlers({ return json(instance, { status: 202 }) }, - projectPolicyView(params) { - const project = lookupProject(params.path) + projectPolicyViewV1({ path, query }) { + const project = lookup.project({ ...path, ...query }) const role_assignments = db.roleAssignments .filter((r) => r.resource_type === 'project' && r.resource_id === project.id) @@ -486,8 +485,8 @@ export const handlers = makeHandlers({ return { role_assignments } }, - projectPolicyUpdate({ body, ...params }) { - const project = lookupProject(params.path) + projectPolicyUpdateV1({ body, path, query }) { + const project = lookup.project({ ...path, ...query }) const newAssignments = body.role_assignments.map((r) => ({ resource_type: 'project' as const, @@ -763,7 +762,7 @@ export const handlers = makeHandlers({ physicalDiskList(params) { return paginated(params.query, db.physicalDisks) }, - policyView() { + policyViewV1() { // assume we're in the default silo const siloId = defaultSilo.id const role_assignments = db.roleAssignments @@ -772,7 +771,7 @@ export const handlers = makeHandlers({ return { role_assignments } }, - policyUpdate({ body }) { + policyUpdateV1({ body }) { const siloId = defaultSilo.id const newAssignments = body.role_assignments.map((r) => ({ resource_type: 'silo' as const, @@ -1038,10 +1037,6 @@ export const handlers = makeHandlers({ instanceSerialConsoleStreamV1: NotImplemented, instanceSerialConsoleV1: NotImplemented, physicalDiskListV1: NotImplemented, - policyUpdateV1: NotImplemented, - policyViewV1: NotImplemented, - projectPolicyUpdateV1: NotImplemented, - projectPolicyViewV1: NotImplemented, rackListV1: NotImplemented, rackViewV1: NotImplemented, sagaListV1: NotImplemented, @@ -1110,9 +1105,13 @@ export const handlers = makeHandlers({ organizationPolicyView: NotImplemented, organizationUpdate: NotImplemented, organizationView: NotImplemented, + policyUpdate: NotImplemented, + policyView: NotImplemented, projectCreate: NotImplemented, projectDelete: NotImplemented, projectList: NotImplemented, + projectPolicyUpdate: NotImplemented, + projectPolicyView: NotImplemented, projectUpdate: NotImplemented, projectView: NotImplemented, snapshotCreate: NotImplemented, From d249daac877776350866be36e462d921d3fa8a8d Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 12:23:09 -0600 Subject: [PATCH 28/39] more cleanup --- app/hooks/use-params.ts | 6 ++++-- app/pages/ProjectsPage.tsx | 42 +++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index ea04e8984..a36dde4c2 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -29,14 +29,16 @@ export const requireSiloParams = requireParams('siloName') export const requireSledParams = requireParams('sledId') export const requireUpdateParams = requireParams('version') -export const useOrgParams = () => requireOrgParams(useParams()) +const useOrgParams = () => requireOrgParams(useParams()) const useProjectParams = () => requireProjectParams(useParams()) -export const useInstanceParams = () => requireInstanceParams(useParams()) +const useInstanceParams = () => requireInstanceParams(useParams()) const useVpcParams = () => requireVpcParams(useParams()) export const useSiloParams = () => requireSiloParams(useParams()) export const useSledParams = () => requireSledParams(useParams()) export const useUpdateParams = () => requireUpdateParams(useParams()) +export const getOrgSelector = (p: Readonly>) => + toApiSelector(requireOrgParams(p)) export const getProjectSelector = (p: Readonly>) => toApiSelector(requireProjectParams(p)) export const getInstanceSelector = (p: Readonly>) => diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index e47aedcc5..88d32ec8e 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -4,8 +4,7 @@ import { Outlet } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom' import type { Project } from '@oxide/api' -import { apiQueryClient } from '@oxide/api' -import { useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' +import { apiQueryClient, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import type { MenuAction } from '@oxide/table' import { DateCell, linkCell, useQueryTable } from '@oxide/table' import { @@ -17,9 +16,9 @@ import { buttonStyle, } from '@oxide/ui' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' -import { requireOrgParams, useOrgParams, useOrgSelector, useQuickActions } from '../hooks' +import { getOrgSelector, useOrgSelector, useQuickActions } from '../hooks' const EmptyState = () => ( ( title="No projects" body="You need to create a project to be able to see it here" buttonText="New project" - buttonTo={pb.projectNew(useOrgParams())} + buttonTo={pb2.projectNew(useOrgSelector())} /> ) ProjectsPage.loader = async ({ params }: LoaderFunctionArgs) => { - const { orgName } = requireOrgParams(params) + const { organization } = getOrgSelector(params) await apiQueryClient.prefetchQuery('projectListV1', { - query: { organization: orgName, limit: 10 }, + query: { organization, limit: 10 }, }) return null } @@ -43,19 +42,18 @@ export default function ProjectsPage() { const navigate = useNavigate() const queryClient = useApiQueryClient() - const { orgName } = useOrgParams() - const orgSelector = useOrgSelector() - const { Table, Column } = useQueryTable('projectListV1', { query: orgSelector }) + const { organization } = useOrgSelector() + const { Table, Column } = useQueryTable('projectListV1', { query: { organization } }) const { data: projects } = useApiQuery('projectListV1', { - query: { ...orgSelector, limit: 10 }, // limit to match QueryTable + query: { ...{ organization }, limit: 10 }, // limit to match QueryTable }) const deleteProject = useApiMutation('projectDeleteV1', { onSuccess() { // TODO: figure out if this is invalidating as expected, can we leave out the query // altogether, etc. Look at whether limit param matters. - queryClient.invalidateQueries('projectListV1', { query: { organization: orgName } }) + queryClient.invalidateQueries('projectListV1', { query: { organization } }) }, }) @@ -63,18 +61,17 @@ export default function ProjectsPage() { { label: 'Edit', onActivate: () => { - const path = { orgName, projectName: project.name } // the edit view has its own loader, but we can make the modal open // instantaneously by preloading the fetch result apiQueryClient.setQueryData( 'projectViewV1', { path: { project: project.name }, - query: orgSelector, + query: { organization }, }, project ) - navigate(pb.projectEdit(path)) + navigate(pb2.projectEdit({ organization, project: project.name })) }, }, { @@ -82,7 +79,7 @@ export default function ProjectsPage() { onActivate: () => { deleteProject.mutate({ path: { project: project.name }, - query: orgSelector, + query: { organization }, }) }, }, @@ -91,14 +88,17 @@ export default function ProjectsPage() { useQuickActions( useMemo( () => [ - { value: 'New project', onSelect: () => navigate(pb.projectNew({ orgName })) }, + { + value: 'New project', + onSelect: () => navigate(pb2.projectNew({ organization })), + }, ...(projects?.items || []).map((p) => ({ value: p.name, - onSelect: () => navigate(pb.instances({ orgName, projectName: p.name })), + onSelect: () => navigate(pb2.instances({ organization, project: p.name })), navGroup: 'Go to project', })), ], - [orgName, navigate, projects] + [organization, navigate, projects] ) ) @@ -108,14 +108,14 @@ export default function ProjectsPage() { }>Projects - + New Project } makeActions={makeActions}> pb.instances({ orgName, projectName }))} + cell={linkCell((project) => pb2.instances({ organization, project }))} /> From 77405fce48ab66afb4fa2d1c96928f82eb4606a9 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 12:33:32 -0600 Subject: [PATCH 29/39] some easy ones --- app/components/TopBarPicker.tsx | 2 +- app/layouts/SystemLayout.tsx | 2 +- app/layouts/helpers.tsx | 2 +- .../instances/instance/tabs/SerialConsoleTab.tsx | 12 ++++++++---- libs/api-mocks/msw/handlers.ts | 16 ++++++++-------- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/components/TopBarPicker.tsx b/app/components/TopBarPicker.tsx index 419961512..158a9b434 100644 --- a/app/components/TopBarPicker.tsx +++ b/app/components/TopBarPicker.tsx @@ -122,7 +122,7 @@ export function useSiloSystemPicker(value: 'silo' | 'system') { // this request in the loader. If that prefetch were removed, fleet viewers // would see the silo picker pop in when the request resolves, which would be // bad. - const { data: systemPolicy } = useApiQuery('systemPolicyView', {}) + const { data: systemPolicy } = useApiQuery('systemPolicyViewV1', {}) return systemPolicy ? : null } diff --git a/app/layouts/SystemLayout.tsx b/app/layouts/SystemLayout.tsx index 9c82fd019..db375335b 100644 --- a/app/layouts/SystemLayout.tsx +++ b/app/layouts/SystemLayout.tsx @@ -31,7 +31,7 @@ import { ContentPane, PageContainer } from './helpers' */ SystemLayout.loader = async () => { const isFleetViewer = await apiQueryClient - .fetchQuery('systemPolicyView', {}) + .fetchQuery('systemPolicyViewV1', {}) .then(() => true) .catch(() => false) diff --git a/app/layouts/helpers.tsx b/app/layouts/helpers.tsx index 2911288b1..877845542 100644 --- a/app/layouts/helpers.tsx +++ b/app/layouts/helpers.tsx @@ -35,7 +35,7 @@ export const userLoader = async () => { // Need to prefetch this because every layout hits it when deciding whether // to show the silo/system picker. It's also fetched by the SystemLayout // loader to figure out whether to 404, but RQ dedupes the request. - apiQueryClient.prefetchQuery('systemPolicyView', {}), + apiQueryClient.prefetchQuery('systemPolicyViewV1', {}), ]) return null } diff --git a/app/pages/project/instances/instance/tabs/SerialConsoleTab.tsx b/app/pages/project/instances/instance/tabs/SerialConsoleTab.tsx index 539a34cae..ac1b16aba 100644 --- a/app/pages/project/instances/instance/tabs/SerialConsoleTab.tsx +++ b/app/pages/project/instances/instance/tabs/SerialConsoleTab.tsx @@ -5,16 +5,20 @@ import { Button } from '@oxide/ui' import { MiB } from '@oxide/util' import { PageActions } from 'app/components/PageActions' -import { useRequiredParams } from 'app/hooks' +import { useInstanceSelector } from 'app/hooks' const Terminal = lazy(() => import('app/components/Terminal')) export function SerialConsoleTab() { - const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') + const { organization, project, instance } = useInstanceSelector() const { data, refetch } = useApiQuery( - 'instanceSerialConsole', - { path: instanceParams, query: { maxBytes: 10 * MiB, fromStart: 0 } }, + 'instanceSerialConsoleV1', + { + path: { instance }, + // holding off on using toPathQuery for now because it doesn't like numbers + query: { organization, project, maxBytes: 10 * MiB, fromStart: 0 }, + }, { refetchOnWindowFocus: false } ) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 2eb55c10f..080774a02 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -460,7 +460,7 @@ export const handlers = makeHandlers({ return json(instance, { status: 202 }) }, - instanceSerialConsole(_params) { + instanceSerialConsoleV1(_params) { // TODO: Add support for params return serial }, @@ -902,7 +902,7 @@ export const handlers = makeHandlers({ userList: (params) => paginated(params.query, db.users), - systemPolicyView() { + systemPolicyViewV1() { const role_assignments = db.roleAssignments .filter((r) => r.resource_type === 'fleet' && r.resource_id === FLEET_ID) .map((r) => pick(r, 'identity_id', 'identity_type', 'role_name')) @@ -1035,7 +1035,6 @@ export const handlers = makeHandlers({ certificateViewV1: NotImplemented, instanceMigrateV1: NotImplemented, instanceSerialConsoleStreamV1: NotImplemented, - instanceSerialConsoleV1: NotImplemented, physicalDiskListV1: NotImplemented, rackListV1: NotImplemented, rackViewV1: NotImplemented, @@ -1045,7 +1044,6 @@ export const handlers = makeHandlers({ sledPhysicalDiskListV1: NotImplemented, sledViewV1: NotImplemented, systemPolicyUpdateV1: NotImplemented, - systemPolicyViewV1: NotImplemented, vpcRouterCreateV1: NotImplemented, vpcRouterDeleteV1: NotImplemented, vpcRouterListV1: NotImplemented, @@ -1056,10 +1054,6 @@ export const handlers = makeHandlers({ vpcRouterRouteViewV1: NotImplemented, vpcRouterUpdateV1: NotImplemented, vpcRouterViewV1: NotImplemented, - vpcSubnetCreate: NotImplemented, - vpcSubnetDelete: NotImplemented, - vpcSubnetList: NotImplemented, - vpcSubnetUpdate: NotImplemented, // deprecated by ID endpoints @@ -1095,6 +1089,7 @@ export const handlers = makeHandlers({ instanceNetworkInterfaceUpdate: NotImplemented, instanceNetworkInterfaceView: NotImplemented, instanceReboot: NotImplemented, + instanceSerialConsole: NotImplemented, instanceStart: NotImplemented, instanceStop: NotImplemented, instanceView: NotImplemented, @@ -1118,9 +1113,14 @@ export const handlers = makeHandlers({ snapshotDelete: NotImplemented, snapshotList: NotImplemented, snapshotView: NotImplemented, + systemPolicyView: NotImplemented, vpcCreate: NotImplemented, vpcDelete: NotImplemented, vpcList: NotImplemented, + vpcSubnetCreate: NotImplemented, + vpcSubnetDelete: NotImplemented, + vpcSubnetList: NotImplemented, + vpcSubnetUpdate: NotImplemented, vpcSubnetView: NotImplemented, vpcUpdate: NotImplemented, vpcView: NotImplemented, From 655b9a99b524ef35c1c268b4c9eea1d0b9acc176 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 12:41:54 -0600 Subject: [PATCH 30/39] vpc router --- app/forms/vpc-router-create.tsx | 14 ++++----- app/forms/vpc-router-edit.tsx | 11 +++---- .../networking/VpcPage/tabs/VpcRoutersTab.tsx | 6 ++-- libs/api-mocks/msw/db.ts | 11 +++++++ libs/api-mocks/msw/handlers.ts | 30 +++++++++---------- libs/api/path-params-v1.ts | 1 + 6 files changed, 43 insertions(+), 30 deletions(-) diff --git a/app/forms/vpc-router-create.tsx b/app/forms/vpc-router-create.tsx index a922ef7ff..7a180241d 100644 --- a/app/forms/vpc-router-create.tsx +++ b/app/forms/vpc-router-create.tsx @@ -3,7 +3,7 @@ import { useApiMutation, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { useRequiredParams, useToast } from 'app/hooks' +import { useToast, useVpcSelector } from 'app/hooks' const defaultValues: VpcRouterCreate = { name: '', @@ -15,17 +15,17 @@ type CreateVpcRouterFormProps = { } export function CreateVpcRouterForm({ onDismiss }: CreateVpcRouterFormProps) { - const parentNames = useRequiredParams('orgName', 'projectName', 'vpcName') + const vpcSelector = useVpcSelector() const queryClient = useApiQueryClient() const addToast = useToast() - const createRouter = useApiMutation('vpcRouterCreate', { + const createRouter = useApiMutation('vpcRouterCreateV1', { onSuccess(router) { - queryClient.invalidateQueries('vpcRouterList', { path: parentNames }) + queryClient.invalidateQueries('vpcRouterListV1', { query: vpcSelector }) // avoid the vpc fetch when the vpc page loads since we have the data queryClient.setQueryData( - 'vpcRouterView', - { path: { ...parentNames, routerName: router.name } }, + 'vpcRouterViewV1', + { path: { router: router.name }, query: vpcSelector }, router ) addToast({ @@ -44,7 +44,7 @@ export function CreateVpcRouterForm({ onDismiss }: CreateVpcRouterFormProps) { formOptions={{ defaultValues }} onDismiss={onDismiss} onSubmit={({ name, description }) => - createRouter.mutate({ path: parentNames, body: { name, description } }) + createRouter.mutate({ query: vpcSelector, body: { name, description } }) } loading={createRouter.isLoading} submitError={createRouter.error} diff --git a/app/forms/vpc-router-edit.tsx b/app/forms/vpc-router-edit.tsx index af51b2a48..bff5052e0 100644 --- a/app/forms/vpc-router-edit.tsx +++ b/app/forms/vpc-router-edit.tsx @@ -3,7 +3,7 @@ import { useApiMutation, useApiQueryClient } from '@oxide/api' import { pick } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { useRequiredParams } from 'app/hooks' +import { useVpcSelector } from 'app/hooks' type EditVpcRouterFormProps = { onDismiss: () => void @@ -11,12 +11,12 @@ type EditVpcRouterFormProps = { } export function EditVpcRouterForm({ onDismiss, editing }: EditVpcRouterFormProps) { - const parentNames = useRequiredParams('orgName', 'projectName', 'vpcName') + const vpcSelector = useVpcSelector() const queryClient = useApiQueryClient() - const updateRouter = useApiMutation('vpcRouterUpdate', { + const updateRouter = useApiMutation('vpcRouterUpdateV1', { onSuccess() { - queryClient.invalidateQueries('vpcRouterList', { path: parentNames }) + queryClient.invalidateQueries('vpcRouterListV1', { query: vpcSelector }) onDismiss() }, }) @@ -31,7 +31,8 @@ export function EditVpcRouterForm({ onDismiss, editing }: EditVpcRouterFormProps onDismiss={onDismiss} onSubmit={({ name, description }) => { updateRouter.mutate({ - path: { ...parentNames, routerName: editing.name }, + path: { router: editing.name }, + query: vpcSelector, body: { name, description }, }) }} diff --git a/app/pages/project/networking/VpcPage/tabs/VpcRoutersTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcRoutersTab.tsx index cf23158ee..76e17dddb 100644 --- a/app/pages/project/networking/VpcPage/tabs/VpcRoutersTab.tsx +++ b/app/pages/project/networking/VpcPage/tabs/VpcRoutersTab.tsx @@ -7,12 +7,12 @@ import { Button, EmptyMessage } from '@oxide/ui' import { CreateVpcRouterForm } from 'app/forms/vpc-router-create' import { EditVpcRouterForm } from 'app/forms/vpc-router-edit' -import { useRequiredParams } from 'app/hooks' +import { useVpcSelector } from 'app/hooks' export const VpcRoutersTab = () => { - const vpcParams = useRequiredParams('orgName', 'projectName', 'vpcName') + const vpcSelector = useVpcSelector() - const { Table, Column } = useQueryTable('vpcRouterList', { path: vpcParams }) + const { Table, Column } = useQueryTable('vpcRouterListV1', { query: vpcSelector }) const [creating, setCreating] = useState(false) const [editing, setEditing] = useState(null) diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index f189b14d4..71bdc0416 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -104,6 +104,17 @@ export const lookup = { return vpc }, + vpcRouter({ router: id, ...vpcSelector }: PPv1.VpcRouter): Json { + if (!id) throw notFoundErr + + if (isUuid(id)) return lookupById(db.vpcRouters, id) + + const vpc = lookup.vpc(vpcSelector) + const router = db.vpcRouters.find((s) => s.vpc_id === vpc.id && s.name === id) + if (!router) throw notFoundErr + + return router + }, vpcSubnet({ subnet: id, ...vpcSelector }: PPv1.VpcSubnet): Json { if (!id) throw notFoundErr diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 080774a02..b6ce40c1f 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -626,13 +626,13 @@ export const handlers = makeHandlers({ return { rules: sortBy(rules, (r) => r.name) } }, - vpcRouterList(params) { - const vpc = lookupVpc(params.path) + vpcRouterListV1({ query }) { + const vpc = lookup.vpc(query) const routers = db.vpcRouters.filter((r) => r.vpc_id === vpc.id) - return paginated(params.query, routers) + return paginated(query, routers) }, - vpcRouterCreate({ body, ...params }) { - const vpc = lookupVpc(params.path) + vpcRouterCreateV1({ body, query }) { + const vpc = lookup.vpc(query) errIfExists(db.vpcRouters, { vpc_id: vpc.id, name: body.name }) const newRouter: Json = { @@ -645,9 +645,9 @@ export const handlers = makeHandlers({ db.vpcRouters.push(newRouter) return json(newRouter, { status: 201 }) }, - vpcRouterView: (params) => lookupVpcRouter(params.path), - vpcRouterUpdate({ body, ...params }) { - const router = lookupVpcRouter(params.path) + vpcRouterViewV1: ({ path, query }) => lookup.vpcRouter({ ...path, ...query }), + vpcRouterUpdateV1({ body, path, query }) { + const router = lookup.vpcRouter({ ...path, ...query }) if (body.name) { router.name = body.name @@ -658,8 +658,8 @@ export const handlers = makeHandlers({ return router }, - vpcRouterDelete(params) { - const router = lookupVpcRouter(params.path) + vpcRouterDeleteV1({ path, query }) { + const router = lookup.vpcRouter({ ...path, ...query }) // TODO: Are there routers that can't be deleted? db.vpcRouters = db.vpcRouters.filter((r) => r.id !== router.id) @@ -1044,16 +1044,11 @@ export const handlers = makeHandlers({ sledPhysicalDiskListV1: NotImplemented, sledViewV1: NotImplemented, systemPolicyUpdateV1: NotImplemented, - vpcRouterCreateV1: NotImplemented, - vpcRouterDeleteV1: NotImplemented, - vpcRouterListV1: NotImplemented, vpcRouterRouteCreateV1: NotImplemented, vpcRouterRouteDeleteV1: NotImplemented, vpcRouterRouteListV1: NotImplemented, vpcRouterRouteUpdateV1: NotImplemented, vpcRouterRouteViewV1: NotImplemented, - vpcRouterUpdateV1: NotImplemented, - vpcRouterViewV1: NotImplemented, // deprecated by ID endpoints @@ -1117,6 +1112,11 @@ export const handlers = makeHandlers({ vpcCreate: NotImplemented, vpcDelete: NotImplemented, vpcList: NotImplemented, + vpcRouterCreate: NotImplemented, + vpcRouterDelete: NotImplemented, + vpcRouterList: NotImplemented, + vpcRouterUpdate: NotImplemented, + vpcRouterView: NotImplemented, vpcSubnetCreate: NotImplemented, vpcSubnetDelete: NotImplemented, vpcSubnetList: NotImplemented, diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index 512448525..8be16834a 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -10,6 +10,7 @@ export type NetworkInterface = Merge export type Snapshot = Merge export type Vpc = Merge export type VpcSubnet = Merge +export type VpcRouter = Merge export type SystemUpdate = { version: string } export type Silo = { silo: string } From 4bd8f47977da8b3018640a9059c5ee295960c56e Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 12:52:54 -0600 Subject: [PATCH 31/39] apparently I shouldn't import things from @oxide/api to @oxide/api-mocks --- app/forms/project-access.tsx | 2 +- app/forms/project-create.tsx | 3 ++- app/forms/project-edit.tsx | 9 ++------- app/forms/vpc-create.tsx | 3 ++- app/forms/vpc-edit.tsx | 5 ++--- app/hooks/use-params.ts | 2 +- app/pages/project/access/ProjectAccessPage.tsx | 3 +-- app/pages/project/instances/actions.tsx | 3 ++- app/pages/project/instances/instance/InstancePage.tsx | 3 ++- app/pages/project/instances/instance/tabs/MetricsTab.tsx | 3 ++- .../project/instances/instance/tabs/NetworkingTab.tsx | 9 ++------- app/pages/project/instances/instance/tabs/StorageTab.tsx | 2 +- app/pages/project/networking/VpcPage/VpcPage.tsx | 4 ++-- libs/api-mocks/msw/handlers.ts | 3 +-- libs/api/index.ts | 2 -- libs/util/index.ts | 1 + libs/{api => util}/selector.spec.ts | 0 libs/{api => util}/selector.ts | 0 18 files changed, 24 insertions(+), 33 deletions(-) rename libs/{api => util}/selector.spec.ts (100%) rename libs/{api => util}/selector.ts (100%) diff --git a/app/forms/project-access.tsx b/app/forms/project-access.tsx index fcd61ad32..fe1463ba8 100644 --- a/app/forms/project-access.tsx +++ b/app/forms/project-access.tsx @@ -1,10 +1,10 @@ import { - toPathQuery, updateRole, useActorsNotInPolicy, useApiMutation, useApiQueryClient, } from '@oxide/api' +import { toPathQuery } from '@oxide/util' import { ListboxField, SideModalForm } from 'app/components/form' import { useProjectSelector } from 'app/hooks' diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index 3004dbf55..3acd76bdf 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -1,8 +1,9 @@ import { useNavigate } from 'react-router-dom' import type { ProjectCreate } from '@oxide/api' -import { toPathQuery, useApiMutation, useApiQueryClient } from '@oxide/api' +import { useApiMutation, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' +import { toPathQuery } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { pb2 } from 'app/util/path-builder' diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index b94acf4af..c7d9e77de 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -1,14 +1,9 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { useNavigate } from 'react-router-dom' -import { - apiQueryClient, - toPathQuery, - useApiMutation, - useApiQuery, - useApiQueryClient, -} from '@oxide/api' +import { apiQueryClient, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' +import { toPathQuery } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { pb2 } from 'app/util/path-builder' diff --git a/app/forms/vpc-create.tsx b/app/forms/vpc-create.tsx index 2980e7b75..c9d7d1f33 100644 --- a/app/forms/vpc-create.tsx +++ b/app/forms/vpc-create.tsx @@ -1,8 +1,9 @@ import { useNavigate } from 'react-router-dom' import type { VpcCreate } from '@oxide/api' -import { toPathQuery, useApiMutation, useApiQueryClient } from '@oxide/api' +import { useApiMutation, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' +import { toPathQuery } from '@oxide/util' import { DescriptionField, NameField, SideModalForm, TextField } from 'app/components/form' import { useProjectSelector, useToast } from 'app/hooks' diff --git a/app/forms/vpc-edit.tsx b/app/forms/vpc-edit.tsx index e535665db..db2ad6235 100644 --- a/app/forms/vpc-edit.tsx +++ b/app/forms/vpc-edit.tsx @@ -1,10 +1,9 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { useNavigate } from 'react-router-dom' -import { toPathQuery, useApiQuery } from '@oxide/api' -import { apiQueryClient, useApiMutation, useApiQueryClient } from '@oxide/api' +import { apiQueryClient, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' -import { pick } from '@oxide/util' +import { pick, toPathQuery } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { getVpcSelector, useToast, useVpcSelector } from 'app/hooks' diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index a36dde4c2..fd3191fd1 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -2,7 +2,7 @@ import type { Params } from 'react-router-dom' import { useParams } from 'react-router-dom' import invariant from 'tiny-invariant' -import { toApiSelector } from '@oxide/api' +import { toApiSelector } from '@oxide/util' const err = (param: string) => `Param '${param}' not found in route. You might be rendering a component under the wrong route.` diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 4745ff21b..19076952d 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -3,7 +3,6 @@ import { useMemo, useState } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' import type { IdentityType, RoleKey } from '@oxide/api' -import { toPathQuery } from '@oxide/api' import { apiQueryClient, byGroupThenName, @@ -24,7 +23,7 @@ import { TableActions, TableEmptyBox, } from '@oxide/ui' -import { groupBy, isTruthy } from '@oxide/util' +import { groupBy, isTruthy, toPathQuery } from '@oxide/util' import { AccessNameCell } from 'app/components/AccessNameCell' import { RoleBadgeCell } from 'app/components/RoleBadgeCell' diff --git a/app/pages/project/instances/actions.tsx b/app/pages/project/instances/actions.tsx index 7167b6b8a..c016082cb 100644 --- a/app/pages/project/instances/actions.tsx +++ b/app/pages/project/instances/actions.tsx @@ -1,9 +1,10 @@ import { useCallback } from 'react' import { useNavigate } from 'react-router-dom' -import { type Instance, toPathQuery, useApiMutation } from '@oxide/api' +import { type Instance, useApiMutation } from '@oxide/api' import type { MakeActions } from '@oxide/table' import { Success16Icon } from '@oxide/ui' +import { toPathQuery } from '@oxide/util' import { useToast } from 'app/hooks' import { pb2 } from 'app/util/path-builder' diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index c045847c4..b5ed203bf 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -3,8 +3,9 @@ import { useMemo } from 'react' import type { LoaderFunctionArgs } from 'react-router-dom' import { useNavigate } from 'react-router-dom' -import { apiQueryClient, toPathQuery, useApiQuery, useApiQueryClient } from '@oxide/api' +import { apiQueryClient, useApiQuery, useApiQueryClient } from '@oxide/api' import { Instances24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui' +import { toPathQuery } from '@oxide/util' import { MoreActionsMenu } from 'app/components/MoreActionsMenu' import { RouteTabs, Tab } from 'app/components/RouteTabs' diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index 974261388..c363d1c41 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -4,8 +4,9 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import invariant from 'tiny-invariant' import type { Cumulativeint64, DiskMetricName } from '@oxide/api' -import { apiQueryClient, toPathQuery, useApiQuery } from '@oxide/api' +import { apiQueryClient, useApiQuery } from '@oxide/api' import { Listbox, Spinner } from '@oxide/ui' +import { toPathQuery } from '@oxide/util' import { useDateTimeRangePicker } from 'app/components/form' import { getInstanceSelector, useInstanceSelector } from 'app/hooks' diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 3aed2f647..369f64820 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -3,13 +3,7 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { Link } from 'react-router-dom' import type { NetworkInterface } from '@oxide/api' -import { - apiQueryClient, - toPathQuery, - useApiMutation, - useApiQuery, - useApiQueryClient, -} from '@oxide/api' +import { apiQueryClient, useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import type { MenuAction } from '@oxide/table' import { useQueryTable } from '@oxide/table' import { @@ -21,6 +15,7 @@ import { OpenLink12Icon, Success12Icon, } from '@oxide/ui' +import { toPathQuery } from '@oxide/util' import CreateNetworkInterfaceForm from 'app/forms/network-interface-create' import EditNetworkInterfaceForm from 'app/forms/network-interface-edit' diff --git a/app/pages/project/instances/instance/tabs/StorageTab.tsx b/app/pages/project/instances/instance/tabs/StorageTab.tsx index b866fa492..961bbaf01 100644 --- a/app/pages/project/instances/instance/tabs/StorageTab.tsx +++ b/app/pages/project/instances/instance/tabs/StorageTab.tsx @@ -4,7 +4,6 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { type Disk, apiQueryClient, - toPathQuery, useApiMutation, useApiQuery, useApiQueryClient, @@ -19,6 +18,7 @@ import { useReactTable, } from '@oxide/table' import { Button, EmptyMessage, Error16Icon, OpenLink12Icon, TableEmptyBox } from '@oxide/ui' +import { toPathQuery } from '@oxide/util' import { DiskStatusBadge } from 'app/components/StatusBadge' import AttachDiskSideModalForm from 'app/forms/disk-attach' diff --git a/app/pages/project/networking/VpcPage/VpcPage.tsx b/app/pages/project/networking/VpcPage/VpcPage.tsx index f1cbfdb59..34adef3f1 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.tsx +++ b/app/pages/project/networking/VpcPage/VpcPage.tsx @@ -1,8 +1,8 @@ import type { LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, toPathQuery, useApiQuery } from '@oxide/api' +import { apiQueryClient, useApiQuery } from '@oxide/api' import { Networking24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui' -import { formatDateTime } from '@oxide/util' +import { formatDateTime, toPathQuery } from '@oxide/util' import { Tab, Tabs } from 'app/components/Tabs' import { getVpcSelector, useVpcSelector } from 'app/hooks' diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index b6ce40c1f..4861358c5 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -1,10 +1,9 @@ import { v4 as uuid } from 'uuid' import type { ApiTypes as Api, UpdateDeployment } from '@oxide/api' -import { toApiSelector } from '@oxide/api' import type { Json } from '@oxide/gen/msw-handlers' import { json, makeHandlers } from '@oxide/gen/msw-handlers' -import { pick, sortBy } from '@oxide/util' +import { pick, sortBy, toApiSelector } from '@oxide/util' import { genCumulativeI64Data, genI64Data } from '../metrics' import { FLEET_ID } from '../role-assignment' diff --git a/libs/api/index.ts b/libs/api/index.ts index 2f2b89b54..1c8295c8a 100644 --- a/libs/api/index.ts +++ b/libs/api/index.ts @@ -50,5 +50,3 @@ export * as PathParamsV1 from './path-params-v1' export type { Params, Result, ResultItem } from './hooks' export { navToLogin } from './nav-to-login' - -export * from './selector' diff --git a/libs/util/index.ts b/libs/util/index.ts index b41aa6449..c56ded77b 100644 --- a/libs/util/index.ts +++ b/libs/util/index.ts @@ -3,5 +3,6 @@ export * from './children' export * from './classed' export * from './date' export * from './object' +export * from './selector' export * from './str' export * from './units' diff --git a/libs/api/selector.spec.ts b/libs/util/selector.spec.ts similarity index 100% rename from libs/api/selector.spec.ts rename to libs/util/selector.spec.ts diff --git a/libs/api/selector.ts b/libs/util/selector.ts similarity index 100% rename from libs/api/selector.ts rename to libs/util/selector.ts From ca3bf7e79b266d3f0101164982848830a58d7bfd Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 13:01:43 -0600 Subject: [PATCH 32/39] fix org access bug --- app/pages/OrgAccessPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pages/OrgAccessPage.tsx b/app/pages/OrgAccessPage.tsx index 7a7022346..7adb9f6b2 100644 --- a/app/pages/OrgAccessPage.tsx +++ b/app/pages/OrgAccessPage.tsx @@ -27,7 +27,7 @@ import { groupBy, isTruthy } from '@oxide/util' import { AccessNameCell } from 'app/components/AccessNameCell' import { RoleBadgeCell } from 'app/components/RoleBadgeCell' import { OrgAccessAddUserSideModal, OrgAccessEditUserSideModal } from 'app/forms/org-access' -import { getProjectSelector, useOrgSelector } from 'app/hooks' +import { getOrgSelector, useOrgSelector } from 'app/hooks' const EmptyState = ({ onClick }: { onClick: () => void }) => ( @@ -45,7 +45,7 @@ OrgAccessPage.loader = async ({ params }: LoaderFunctionArgs) => { await Promise.all([ apiQueryClient.prefetchQuery('policyViewV1', {}), apiQueryClient.prefetchQuery('organizationPolicyViewV1', { - path: getProjectSelector(params), + path: getOrgSelector(params), }), // used to resolve user names apiQueryClient.prefetchQuery('userList', {}), From 1428e652c63cc44ffd4599461586b383bc31070c Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 13:44:15 -0600 Subject: [PATCH 33/39] sleds and physical disks and stuff --- app/pages/system/InventoryPage/DisksTab.tsx | 6 ++-- .../system/InventoryPage/InventoryPage.tsx | 4 +-- app/pages/system/InventoryPage/RacksTab.tsx | 4 +-- app/pages/system/InventoryPage/SledsTab.tsx | 4 +-- libs/api-mocks/msw/db.ts | 11 ++++--- libs/api-mocks/msw/handlers.ts | 30 ++++++++----------- 6 files changed, 26 insertions(+), 33 deletions(-) diff --git a/app/pages/system/InventoryPage/DisksTab.tsx b/app/pages/system/InventoryPage/DisksTab.tsx index 6f896d5e1..96f96b21c 100644 --- a/app/pages/system/InventoryPage/DisksTab.tsx +++ b/app/pages/system/InventoryPage/DisksTab.tsx @@ -13,14 +13,12 @@ const EmptyState = () => { } DisksTab.loader = async () => { - await apiQueryClient.prefetchQuery('physicalDiskList', { - query: { limit: 10 }, - }) + await apiQueryClient.prefetchQuery('physicalDiskListV1', { query: { limit: 10 } }) return null } export function DisksTab() { - const { Table, Column } = useQueryTable('physicalDiskList', {}) + const { Table, Column } = useQueryTable('physicalDiskListV1', {}) return ( <> diff --git a/app/pages/system/InventoryPage/InventoryPage.tsx b/app/pages/system/InventoryPage/InventoryPage.tsx index 2db9ad15e..2a170d790 100644 --- a/app/pages/system/InventoryPage/InventoryPage.tsx +++ b/app/pages/system/InventoryPage/InventoryPage.tsx @@ -7,14 +7,14 @@ import { RouteTabs, Tab } from 'app/components/RouteTabs' import { pb } from 'app/util/path-builder' InventoryPage.loader = async () => { - await apiQueryClient.prefetchQuery('rackList', { + await apiQueryClient.prefetchQuery('rackListV1', { query: { limit: 10 }, }) return null } export function InventoryPage() { - const { data: racks } = useApiQuery('rackList', { query: { limit: 10 } }) + const { data: racks } = useApiQuery('rackListV1', { query: { limit: 10 } }) const rack = racks?.items[0] // TODO: Add loading state diff --git a/app/pages/system/InventoryPage/RacksTab.tsx b/app/pages/system/InventoryPage/RacksTab.tsx index ad100f494..15efc7804 100644 --- a/app/pages/system/InventoryPage/RacksTab.tsx +++ b/app/pages/system/InventoryPage/RacksTab.tsx @@ -13,14 +13,14 @@ const EmptyState = () => { } RacksTab.loader = async () => { - await apiQueryClient.prefetchQuery('rackList', { + await apiQueryClient.prefetchQuery('rackListV1', { query: { limit: 10 }, }) return null } export function RacksTab() { - const { Table, Column } = useQueryTable('rackList', {}) + const { Table, Column } = useQueryTable('rackListV1', {}) return ( <> diff --git a/app/pages/system/InventoryPage/SledsTab.tsx b/app/pages/system/InventoryPage/SledsTab.tsx index 19d7afbcf..fc410c186 100644 --- a/app/pages/system/InventoryPage/SledsTab.tsx +++ b/app/pages/system/InventoryPage/SledsTab.tsx @@ -13,14 +13,14 @@ const EmptyState = () => { } SledsTab.loader = async () => { - await apiQueryClient.prefetchQuery('sledList', { + await apiQueryClient.prefetchQuery('sledListV1', { query: { limit: 10 }, }) return null } export function SledsTab() { - const { Table, Column } = useQueryTable('sledList', {}, { keepPreviousData: true }) + const { Table, Column } = useQueryTable('sledListV1', {}, { keepPreviousData: true }) return ( <> diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 71bdc0416..a7927fffa 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -131,6 +131,11 @@ export const lookup = { if (!update) throw notFoundErr return update }, + sled(params: PP.Id): Json { + const sled = db.sleds.find((sled) => sled.id === params.id) + if (!sled) throw notFoundErr + return sled + }, } export function lookupOrg(params: PP.Org): Json { @@ -225,12 +230,6 @@ export function lookupSshKey(params: PP.SshKey): Json { return sshKey } -export function lookupSled(params: PP.Id): Json { - const sled = db.sleds.find((sled) => sled.id === params.id) - if (!sled) throw notFoundErr - return sled -} - const initDb = { disks: [...mock.disks], globalImages: [...mock.globalImages], diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 4861358c5..b43c8ae1d 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -19,7 +19,6 @@ import { lookupImage, lookupSamlIdp, lookupSilo, - lookupSled, lookupSshKey, lookupVpc, lookupVpcRouter, @@ -753,14 +752,12 @@ export const handlers = makeHandlers({ const nics = db.networkInterfaces.filter((n) => n.subnet_id === subnet.id) return paginated(params.query, nics) }, - sledPhysicalDiskList(params) { - const sled = lookupSled({ id: params.path.sledId }) + sledPhysicalDiskListV1({ path, query }) { + const sled = lookup.sled({ id: path.sledId }) const disks = db.physicalDisks.filter((n) => n.sled_id === sled.id) - return paginated(params.query, disks) - }, - physicalDiskList(params) { - return paginated(params.query, db.physicalDisks) + return paginated(query, disks) }, + physicalDiskListV1: ({ query }) => paginated(query, db.physicalDisks), policyViewV1() { // assume we're in the default silo const siloId = defaultSilo.id @@ -786,9 +783,7 @@ export const handlers = makeHandlers({ return body }, - rackList(params) { - return paginated(params.query, db.racks) - }, + rackListV1: ({ query }) => paginated(query, db.racks), sessionMe() { return user1 }, @@ -820,7 +815,7 @@ export const handlers = makeHandlers({ db.sshKeys = db.sshKeys.filter((i) => i.id !== sshKey.id) return 204 }, - sledList: (params) => paginated(params.query, db.sleds), + sledListV1: (params) => paginated(params.query, db.sleds), systemImageList: (params) => paginated(params.query, db.globalImages), systemImageCreate({ body }) { errIfExists(db.globalImages, { name: body.name }) @@ -1011,7 +1006,6 @@ export const handlers = makeHandlers({ loginSamlBegin: NotImplemented, loginSpoof: NotImplemented, logout: NotImplemented, - rackView: NotImplemented, roleList: NotImplemented, roleView: NotImplemented, sagaList: NotImplemented, @@ -1026,7 +1020,7 @@ export const handlers = makeHandlers({ systemUserView: NotImplemented, timeseriesSchemaGet: NotImplemented, - // V1 endpoints + // V1 endpoints we're not using in the console yet certificateCreateV1: NotImplemented, certificateDeleteV1: NotImplemented, @@ -1034,15 +1028,12 @@ export const handlers = makeHandlers({ certificateViewV1: NotImplemented, instanceMigrateV1: NotImplemented, instanceSerialConsoleStreamV1: NotImplemented, - physicalDiskListV1: NotImplemented, - rackListV1: NotImplemented, rackViewV1: NotImplemented, sagaListV1: NotImplemented, sagaViewV1: NotImplemented, - sledListV1: NotImplemented, - sledPhysicalDiskListV1: NotImplemented, sledViewV1: NotImplemented, systemPolicyUpdateV1: NotImplemented, + vpcRouterRouteCreateV1: NotImplemented, vpcRouterRouteDeleteV1: NotImplemented, vpcRouterRouteListV1: NotImplemented, @@ -1094,6 +1085,7 @@ export const handlers = makeHandlers({ organizationPolicyView: NotImplemented, organizationUpdate: NotImplemented, organizationView: NotImplemented, + physicalDiskList: NotImplemented, policyUpdate: NotImplemented, policyView: NotImplemented, projectCreate: NotImplemented, @@ -1103,6 +1095,10 @@ export const handlers = makeHandlers({ projectPolicyView: NotImplemented, projectUpdate: NotImplemented, projectView: NotImplemented, + rackList: NotImplemented, + rackView: NotImplemented, + sledList: NotImplemented, + sledPhysicalDiskList: NotImplemented, snapshotCreate: NotImplemented, snapshotDelete: NotImplemented, snapshotList: NotImplemented, From 47e6a60e97e12ba9f5114fb59063b94ad73c3c74 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 13:56:32 -0600 Subject: [PATCH 34/39] router routes and some cleanup --- .../VpcPage/tabs/VpcSystemRoutesTab.tsx | 9 ++- libs/api-mocks/msw/db.ts | 67 +++++-------------- libs/api-mocks/msw/handlers.ts | 38 +++++------ libs/api/path-params-v1.ts | 1 + 4 files changed, 40 insertions(+), 75 deletions(-) diff --git a/app/pages/project/networking/VpcPage/tabs/VpcSystemRoutesTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcSystemRoutesTab.tsx index c3b5bf65d..d4c024823 100644 --- a/app/pages/project/networking/VpcPage/tabs/VpcSystemRoutesTab.tsx +++ b/app/pages/project/networking/VpcPage/tabs/VpcSystemRoutesTab.tsx @@ -1,7 +1,7 @@ import { TypeValueCell, useQueryTable } from '@oxide/table' import { EmptyMessage } from '@oxide/ui' -import { useRequiredParams } from 'app/hooks' +import { useVpcSelector } from 'app/hooks' const EmptyState = () => ( ( ) export const VpcSystemRoutesTab = () => { - const vpcParams = useRequiredParams('orgName', 'projectName', 'vpcName') - - const { Table, Column } = useQueryTable('vpcRouterRouteList', { - path: { routerName: 'system', ...vpcParams }, + const vpcSelector = useVpcSelector() + const { Table, Column } = useQueryTable('vpcRouterRouteListV1', { + query: { ...vpcSelector, router: 'system' }, }) return ( diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index a7927fffa..dbe5b31dd 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -5,6 +5,7 @@ import { validate as isUuid } from 'uuid' import * as mock from '@oxide/api-mocks' import type { ApiTypes as Api, PathParams as PP, PathParamsV1 as PPv1 } from '@oxide/api' import { user1 } from '@oxide/api-mocks' +import { toApiSelector } from '@oxide/util' import type { Json } from '../json-type' import { clone, json } from './util' @@ -115,6 +116,22 @@ export const lookup = { return router }, + vpcRouterRoute({ + route: id, + ...routerSelector + }: PPv1.RouterRoute): Json { + if (!id) throw notFoundErr + + if (isUuid(id)) return lookupById(db.vpcRouterRoutes, id) + + const router = lookup.vpcRouter(routerSelector) + const route = db.vpcRouterRoutes.find( + (s) => s.vpc_router_id === router.id && s.name === id + ) + if (!route) throw notFoundErr + + return route + }, vpcSubnet({ subnet: id, ...vpcSelector }: PPv1.VpcSubnet): Json { if (!id) throw notFoundErr @@ -138,34 +155,8 @@ export const lookup = { }, } -export function lookupOrg(params: PP.Org): Json { - const org = db.orgs.find((o) => o.name === params.orgName) - if (!org) throw notFoundErr - return org -} - -export function lookupProject(params: PP.Project): Json { - const org = lookupOrg(params) - - const project = db.projects.find( - (p) => p.organization_id === org.id && p.name === params.projectName - ) - if (!project) throw notFoundErr - - return project -} - -export function lookupVpc(params: PP.Vpc): Json { - const project = lookupProject(params) - - const vpc = db.vpcs.find((p) => p.project_id === project.id && p.name === params.vpcName) - if (!vpc) throw notFoundErr - - return vpc -} - export function lookupImage(params: PP.Image): Json { - const project = lookupProject(params) + const project = lookup.project(toApiSelector(params)) const image = db.images.find( (d) => d.project_id === project.id && d.name === params.imageName @@ -175,28 +166,6 @@ export function lookupImage(params: PP.Image): Json { return image } -export function lookupVpcRouter(params: PP.VpcRouter): Json { - const vpc = lookupVpc(params) - - const router = db.vpcRouters.find( - (r) => r.vpc_id === vpc.id && r.name === params.routerName - ) - if (!router) throw notFoundErr - - return router -} - -export function lookupVpcRouterRoute(params: PP.VpcRouterRoute): Json { - const router = lookupVpcRouter(params) - - const route = db.vpcRouterRoutes.find( - (r) => r.vpc_router_id === router.id && r.name === params.routeName - ) - if (!route) throw notFoundErr - - return route -} - export function lookupGlobalImage(params: PP.GlobalImage): Json { const image = db.globalImages.find((o) => o.name === params.imageName) if (!image) throw notFoundErr diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index b43c8ae1d..15026a3a4 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -20,9 +20,6 @@ import { lookupSamlIdp, lookupSilo, lookupSshKey, - lookupVpc, - lookupVpcRouter, - lookupVpcRouterRoute, } from './db' import { NotImplemented, @@ -601,13 +598,13 @@ export const handlers = makeHandlers({ return 204 }, vpcFirewallRulesView(params) { - const vpc = lookupVpc(params.path) + const vpc = lookup.vpc(toApiSelector(params.path)) const rules = db.vpcFirewallRules.filter((r) => r.vpc_id === vpc.id) return { rules: sortBy(rules, (r) => r.name) } }, vpcFirewallRulesUpdate({ body, ...params }) { - const vpc = lookupVpc(params.path) + const vpc = lookup.vpc(toApiSelector(params.path)) const rules = body.rules.map((rule) => ({ vpc_id: vpc.id, @@ -664,13 +661,13 @@ export const handlers = makeHandlers({ return 204 }, - vpcRouterRouteList(params) { - const router = lookupVpcRouter(params.path) + vpcRouterRouteListV1({ query }) { + const router = lookup.vpcRouter(query) const routers = db.vpcRouterRoutes.filter((s) => s.vpc_router_id === router.id) - return paginated(params.query, routers) + return paginated(query, routers) }, - vpcRouterRouteCreate({ body, ...params }) { - const router = lookupVpcRouter(params.path) + vpcRouterRouteCreateV1({ body, query }) { + const router = lookup.vpcRouter(query) errIfExists(db.vpcRouterRoutes, { vpc_router_id: router.id, name: body.name }) @@ -683,9 +680,9 @@ export const handlers = makeHandlers({ } return json(newRoute, { status: 201 }) }, - vpcRouterRouteView: (params) => lookupVpcRouterRoute(params.path), - vpcRouterRouteUpdate({ body, ...params }) { - const route = lookupVpcRouterRoute(params.path) + vpcRouterRouteViewV1: ({ path, query }) => lookup.vpcRouterRoute({ ...path, ...query }), + vpcRouterRouteUpdateV1({ body, path, query }) { + const route = lookup.vpcRouterRoute({ ...path, ...query }) if (route.kind !== 'custom') { throw 'Only custom routes may be modified' } @@ -697,8 +694,8 @@ export const handlers = makeHandlers({ } return route }, - vpcRouterRouteDelete(params) { - const route = lookupVpcRouterRoute(params.path) + vpcRouterRouteDeleteV1({ path, query }) { + const route = lookup.vpcRouterRoute({ ...path, ...query }) if (route.kind !== 'custom') { throw 'Only custom routes may be modified' } @@ -1034,12 +1031,6 @@ export const handlers = makeHandlers({ sledViewV1: NotImplemented, systemPolicyUpdateV1: NotImplemented, - vpcRouterRouteCreateV1: NotImplemented, - vpcRouterRouteDeleteV1: NotImplemented, - vpcRouterRouteListV1: NotImplemented, - vpcRouterRouteUpdateV1: NotImplemented, - vpcRouterRouteViewV1: NotImplemented, - // deprecated by ID endpoints diskViewById: NotImplemented, @@ -1112,6 +1103,11 @@ export const handlers = makeHandlers({ vpcRouterList: NotImplemented, vpcRouterUpdate: NotImplemented, vpcRouterView: NotImplemented, + vpcRouterRouteCreate: NotImplemented, + vpcRouterRouteDelete: NotImplemented, + vpcRouterRouteList: NotImplemented, + vpcRouterRouteUpdate: NotImplemented, + vpcRouterRouteView: NotImplemented, vpcSubnetCreate: NotImplemented, vpcSubnetDelete: NotImplemented, vpcSubnetList: NotImplemented, diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts index 8be16834a..9d5d5282f 100644 --- a/libs/api/path-params-v1.ts +++ b/libs/api/path-params-v1.ts @@ -11,6 +11,7 @@ export type Snapshot = Merge export type Vpc = Merge export type VpcSubnet = Merge export type VpcRouter = Merge +export type RouterRoute = Merge export type SystemUpdate = { version: string } export type Silo = { silo: string } From 1750083f51b06a184b684aa131f1841a5435656f Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 14:16:41 -0600 Subject: [PATCH 35/39] convert remaining pb to pb2 --- app/components/TopBar.tsx | 4 +- app/components/TopBarPicker.tsx | 10 +-- app/forms/idp-create.tsx | 4 +- app/forms/org-create.tsx | 6 +- app/forms/org-edit.tsx | 4 +- app/forms/silo-create.tsx | 4 +- app/forms/ssh-key-create.tsx | 4 +- app/layouts/OrgLayout.tsx | 14 +-- app/layouts/ProjectLayout.tsx | 25 +++--- app/layouts/SettingsLayout.tsx | 6 +- app/layouts/SiloLayout.tsx | 8 +- app/layouts/SystemLayout.tsx | 18 ++-- app/pages/DeviceAuthVerifyPage.tsx | 4 +- app/pages/LoginPage.tsx | 4 +- app/pages/__tests__/silos.e2e.ts | 4 +- app/pages/__tests__/top-bar.e2e.ts | 4 +- .../instances/instance/tabs/NetworkingTab.tsx | 7 +- app/pages/settings/SSHKeysPage.tsx | 6 +- .../system/InventoryPage/InventoryPage.tsx | 6 +- app/pages/system/SiloPage.tsx | 7 +- app/pages/system/SilosPage.tsx | 12 +-- app/pages/system/UpdateDetailSideModal.tsx | 4 +- app/pages/system/UpdatePage.tsx | 10 +-- app/routes.tsx | 4 +- app/test/instance-create.e2e.ts | 12 ++- app/util/path-builder.spec.ts | 14 +-- app/util/path-builder.ts | 87 +------------------ libs/api-mocks/msw/db.ts | 2 +- 28 files changed, 112 insertions(+), 182 deletions(-) diff --git a/app/components/TopBar.tsx b/app/components/TopBar.tsx index 08bb6b30e..a22723133 100644 --- a/app/components/TopBar.tsx +++ b/app/components/TopBar.tsx @@ -11,7 +11,7 @@ import { Profile16Icon, } from '@oxide/ui' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' export function TopBar({ children }: { children: React.ReactNode }) { const navigate = useNavigate() @@ -71,7 +71,7 @@ export function TopBar({ children }: { children: React.ReactNode }) { - navigate(pb.settings())}>Settings + navigate(pb2.settings())}>Settings {loggedIn ? ( logout.mutate({})}>Sign out ) : ( diff --git a/app/components/TopBarPicker.tsx b/app/components/TopBarPicker.tsx index 158a9b434..99d5fa49a 100644 --- a/app/components/TopBarPicker.tsx +++ b/app/components/TopBarPicker.tsx @@ -6,7 +6,7 @@ import { useApiQuery } from '@oxide/api' import { Identicon, Organization16Icon, SelectArrows6Icon, Success12Icon } from '@oxide/ui' import { useInstanceSelector, useProjectSelector, useSiloParams } from 'app/hooks' -import { pb, pb2 } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' type TopBarPickerItem = { label: string @@ -130,8 +130,8 @@ export function useSiloSystemPicker(value: 'silo' | 'system') { export function SiloSystemPicker({ value }: { value: 'silo' | 'system' }) { const commonProps = { items: [ - { label: 'System', to: pb.silos() }, - { label: 'Silo', to: pb.orgs() }, + { label: 'System', to: pb2.silos() }, + { label: 'Silo', to: pb2.orgs() }, ], 'aria-label': 'Switch between system and silo', } @@ -157,7 +157,7 @@ export function SiloPicker() { const { data } = useApiQuery('siloList', { query: { limit: 10 } }) const items = (data?.items || []).map((silo) => ({ label: silo.name, - to: pb.silo({ siloName: silo.name }), + to: pb2.silo({ silo: silo.name }), })) return ( @@ -177,7 +177,7 @@ export function OrgPicker() { const { data } = useApiQuery('organizationListV1', { query: { limit: 20 } }) const items = (data?.items || []).map(({ name }) => ({ label: name, - to: pb.projects({ orgName: name }), + to: pb2.projects({ organization: name }), })) return ( diff --git a/app/forms/idp-create.tsx b/app/forms/idp-create.tsx index 6c3f781a7..b2da146d8 100644 --- a/app/forms/idp-create.tsx +++ b/app/forms/idp-create.tsx @@ -13,7 +13,7 @@ import { TextField, TextFieldInner, } from 'app/components/form' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' import { useSiloParams, useToast } from '../hooks' @@ -43,7 +43,7 @@ export function CreateIdpSideModalForm() { const { siloName } = useSiloParams() - const onDismiss = () => navigate(pb.silo({ siloName })) + const onDismiss = () => navigate(pb2.silo({ silo: siloName })) const createIdp = useApiMutation('samlIdentityProviderCreate', { onSuccess() { diff --git a/app/forms/org-create.tsx b/app/forms/org-create.tsx index f6968bc0d..251bffdaa 100644 --- a/app/forms/org-create.tsx +++ b/app/forms/org-create.tsx @@ -6,7 +6,7 @@ import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { useToast } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' const defaultValues: OrganizationCreate = { name: '', @@ -32,7 +32,7 @@ export function CreateOrgSideModalForm() { title: 'Success!', content: 'Your organization has been created.', }) - navigate(pb.projects({ orgName: org.name })) + navigate(pb2.projects({ organization: org.name })) }, }) @@ -41,7 +41,7 @@ export function CreateOrgSideModalForm() { id="create-org-form" formOptions={{ defaultValues }} title="Create organization" - onDismiss={() => navigate(pb.orgs())} + onDismiss={() => navigate(pb2.orgs())} onSubmit={(values) => createOrg.mutate({ body: values })} loading={createOrg.isLoading} submitError={createOrg.error} diff --git a/app/forms/org-edit.tsx b/app/forms/org-edit.tsx index d2a8a6019..e9c0aaf57 100644 --- a/app/forms/org-edit.tsx +++ b/app/forms/org-edit.tsx @@ -6,7 +6,7 @@ import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { requireOrgParams, useOrgSelector, useToast } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' EditOrgSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { const { orgName } = requireOrgParams(params) @@ -23,7 +23,7 @@ export function EditOrgSideModalForm() { const { organization } = useOrgSelector() - const onDismiss = () => navigate(pb.orgs()) + const onDismiss = () => navigate(pb2.orgs()) const { data: org } = useApiQuery('organizationViewV1', { path: { organization } }) diff --git a/app/forms/silo-create.tsx b/app/forms/silo-create.tsx index e1058f5ed..b5ab8346b 100644 --- a/app/forms/silo-create.tsx +++ b/app/forms/silo-create.tsx @@ -12,7 +12,7 @@ import { SideModalForm, } from 'app/components/form' import { useToast } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' const defaultValues: SiloCreate = { name: '', @@ -26,7 +26,7 @@ export function CreateSiloSideModalForm() { const queryClient = useApiQueryClient() const addToast = useToast() - const onDismiss = () => navigate(pb.silos()) + const onDismiss = () => navigate(pb2.silos()) const createSilo = useApiMutation('siloCreate', { onSuccess(silo) { diff --git a/app/forms/ssh-key-create.tsx b/app/forms/ssh-key-create.tsx index bbb837900..8fe6d188e 100644 --- a/app/forms/ssh-key-create.tsx +++ b/app/forms/ssh-key-create.tsx @@ -4,7 +4,7 @@ import type { SshKeyCreate } from '@oxide/api' import { useApiMutation, useApiQueryClient } from '@oxide/api' import { DescriptionField, NameField, SideModalForm, TextField } from 'app/components/form' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' const defaultValues: SshKeyCreate = { name: '', @@ -16,7 +16,7 @@ export function CreateSSHKeySideModalForm() { const queryClient = useApiQueryClient() const navigate = useNavigate() - const onDismiss = () => navigate(pb.sshKeys()) + const onDismiss = () => navigate(pb2.sshKeys()) const createSshKey = useApiMutation('sessionSshkeyCreate', { onSuccess() { diff --git a/app/layouts/OrgLayout.tsx b/app/layouts/OrgLayout.tsx index 27d48ec81..24dbebb7a 100644 --- a/app/layouts/OrgLayout.tsx +++ b/app/layouts/OrgLayout.tsx @@ -2,14 +2,14 @@ import { Access16Icon, Divider, Folder16Icon, Organization16Icon } from '@oxide/ import { TopBar } from 'app/components/TopBar' import { OrgPicker, useSiloSystemPicker } from 'app/components/TopBarPicker' -import { useRequiredParams } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { useOrgSelector } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' import { DocsLinkItem, NavLinkItem, Sidebar } from '../components/Sidebar' import { ContentPane, PageContainer } from './helpers' const OrgLayout = () => { - const { orgName } = useRequiredParams('orgName') + const { organization } = useOrgSelector() return ( @@ -19,19 +19,19 @@ const OrgLayout = () => { - + Organizations - - + + Projects - + Access & IAM diff --git a/app/layouts/ProjectLayout.tsx b/app/layouts/ProjectLayout.tsx index a67d0c732..a27334f33 100644 --- a/app/layouts/ProjectLayout.tsx +++ b/app/layouts/ProjectLayout.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { matchPath, useLocation, useNavigate } from 'react-router-dom' +import { matchPath, useLocation, useNavigate, useParams } from 'react-router-dom' import { Access16Icon, @@ -19,8 +19,8 @@ import { ProjectPicker, useSiloSystemPicker, } from 'app/components/TopBarPicker' -import { useAllParams, useQuickActions } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { useProjectSelector, useQuickActions } from 'app/hooks' +import { pb2 } from 'app/util/path-builder' import { DocsLinkItem, NavLinkItem, Sidebar } from '../components/Sidebar' import { ContentPane, PageContainer } from './helpers' @@ -28,8 +28,9 @@ import { ContentPane, PageContainer } from './helpers' const ProjectLayout = () => { const navigate = useNavigate() // org and project will always be there, instance may not - const { instanceName, orgName, projectName } = useAllParams('orgName', 'projectName') - const projectParams = { orgName, projectName } + const projectSelector = useProjectSelector() + const { project: projectName } = projectSelector + const { instanceName } = useParams() const currentPath = useLocation().pathname useQuickActions( useMemo( @@ -64,7 +65,7 @@ const ProjectLayout = () => { - + Projects @@ -72,22 +73,22 @@ const ProjectLayout = () => { - + Instances - + Snapshots - + Disks - + Access & IAM - + Images - + Networking diff --git a/app/layouts/SettingsLayout.tsx b/app/layouts/SettingsLayout.tsx index 640027331..37a0bc75d 100644 --- a/app/layouts/SettingsLayout.tsx +++ b/app/layouts/SettingsLayout.tsx @@ -6,7 +6,7 @@ import { Divider, Key16Icon, Profile16Icon } from '@oxide/ui' import { TopBar } from 'app/components/TopBar' import { OrgPicker, useSiloSystemPicker } from 'app/components/TopBarPicker' import { useQuickActions } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' import { DocsLinkItem, NavLinkItem, Sidebar } from '../components/Sidebar' import { ContentPane, PageContainer } from './helpers' @@ -47,10 +47,10 @@ const SettingsLayout = () => { - + Profile - + SSH Keys diff --git a/app/layouts/SiloLayout.tsx b/app/layouts/SiloLayout.tsx index cc07e4565..9e6483903 100644 --- a/app/layouts/SiloLayout.tsx +++ b/app/layouts/SiloLayout.tsx @@ -3,7 +3,7 @@ import { Access16Icon, Divider, Organization16Icon, Snapshots16Icon } from '@oxi import { DocsLinkItem, NavLinkItem, Sidebar } from 'app/components/Sidebar' import { TopBar } from 'app/components/TopBar' import { OrgPicker, useSiloSystemPicker } from 'app/components/TopBarPicker' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' import { ContentPane, PageContainer } from './helpers' @@ -21,13 +21,13 @@ export default function SiloLayout() { {/* TODO: silo name in heading */} - + Organizations - + Utilization - + Access & IAM diff --git a/app/layouts/SystemLayout.tsx b/app/layouts/SystemLayout.tsx index db375335b..d2d64070d 100644 --- a/app/layouts/SystemLayout.tsx +++ b/app/layouts/SystemLayout.tsx @@ -17,7 +17,7 @@ import { trigger404 } from 'app/components/ErrorBoundary' import { DocsLinkItem, NavLinkItem, Sidebar } from 'app/components/Sidebar' import { TopBar } from 'app/components/TopBar' import { SiloPicker, SiloSystemPicker } from 'app/components/TopBarPicker' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' import { ContentPane, PageContainer } from './helpers' @@ -59,28 +59,28 @@ export default function SystemLayout() { - + Silos - + Issues - + Utilization - + Inventory - + Health - + System Update - + Networking - + Settings diff --git a/app/pages/DeviceAuthVerifyPage.tsx b/app/pages/DeviceAuthVerifyPage.tsx index d0166519b..65db3fcb1 100644 --- a/app/pages/DeviceAuthVerifyPage.tsx +++ b/app/pages/DeviceAuthVerifyPage.tsx @@ -3,7 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom' import { useApiMutation } from '@oxide/api' import { Button, Warning12Icon } from '@oxide/ui' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' import { useToast } from '../hooks' @@ -16,7 +16,7 @@ export default function DeviceAuthVerifyPage() { const addToast = useToast() const confirmPost = useApiMutation('deviceAuthConfirm', { onSuccess: () => { - navigate(pb.deviceSuccess()) + navigate(pb2.deviceSuccess()) }, onError: () => { addToast({ diff --git a/app/pages/LoginPage.tsx b/app/pages/LoginPage.tsx index 12b78058b..90371f268 100644 --- a/app/pages/LoginPage.tsx +++ b/app/pages/LoginPage.tsx @@ -3,7 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom' import { useApiMutation } from '@oxide/api' import { Button, Success16Icon, Warning12Icon } from '@oxide/ui' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' import { useToast } from '../hooks' @@ -30,7 +30,7 @@ export default function LoginPage() { title: 'Logged in', icon: , }) - navigate(searchParams.get('state') || pb.orgs()) + navigate(searchParams.get('state') || pb2.orgs()) }, onError: () => { addToast({ diff --git a/app/pages/__tests__/silos.e2e.ts b/app/pages/__tests__/silos.e2e.ts index 87a69bdaa..cf1e8675b 100644 --- a/app/pages/__tests__/silos.e2e.ts +++ b/app/pages/__tests__/silos.e2e.ts @@ -1,10 +1,10 @@ import { expect, test } from '@playwright/test' import { expectNotVisible, expectRowVisible, expectVisible } from 'app/test/e2e' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' test('Silos page', async ({ page }) => { - await page.goto(pb.silos()) + await page.goto(pb2.silos()) await expectVisible(page, ['role=heading[name*="Silos"]']) const table = page.locator('role=table') diff --git a/app/pages/__tests__/top-bar.e2e.ts b/app/pages/__tests__/top-bar.e2e.ts index b97d6daad..1b3db6c60 100644 --- a/app/pages/__tests__/top-bar.e2e.ts +++ b/app/pages/__tests__/top-bar.e2e.ts @@ -1,10 +1,10 @@ import { test } from '@playwright/test' import { expectSimultaneous } from 'app/test/e2e' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' test('Silo/system picker does not pop in', async ({ page }) => { - await page.goto(pb.orgs()) + await page.goto(pb2.orgs()) // make sure the system policy call is prefetched properly so that the // silo/system picker doesn't pop in. if this turns out to be flaky, just diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 369f64820..30e982011 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -22,19 +22,20 @@ import EditNetworkInterfaceForm from 'app/forms/network-interface-edit' import { getInstanceSelector, useInstanceSelector, + useProjectSelector, useRequiredParams, useToast, } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' const VpcNameFromId = ({ value }: { value: string }) => { - const { orgName, projectName } = useRequiredParams('orgName', 'projectName') + const projectSelector = useProjectSelector() const { data: vpc } = useApiQuery('vpcViewV1', { path: { vpc: value } }) if (!vpc) return null return ( {vpc.name} diff --git a/app/pages/settings/SSHKeysPage.tsx b/app/pages/settings/SSHKeysPage.tsx index 3e88bd812..09ac7c05a 100644 --- a/app/pages/settings/SSHKeysPage.tsx +++ b/app/pages/settings/SSHKeysPage.tsx @@ -16,7 +16,7 @@ import { buttonStyle, } from '@oxide/ui' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' SSHKeysPage.loader = async () => { await apiQueryClient.prefetchQuery('sessionSshkeyList', { query: { limit: 10 } }) @@ -53,7 +53,7 @@ export function SSHKeysPage() { }>SSH Keys - + Add SSH key @@ -65,7 +65,7 @@ export function SSHKeysPage() { title="No SSH keys" body="You need to create an ssh key to be able to see it here" buttonText="Add SSH key" - onClick={() => navigate(pb.sshKeyNew())} + onClick={() => navigate(pb2.sshKeyNew())} /> } > diff --git a/app/pages/system/InventoryPage/InventoryPage.tsx b/app/pages/system/InventoryPage/InventoryPage.tsx index 2a170d790..cd97292d7 100644 --- a/app/pages/system/InventoryPage/InventoryPage.tsx +++ b/app/pages/system/InventoryPage/InventoryPage.tsx @@ -4,7 +4,7 @@ import { apiQueryClient, useApiQuery } from '@oxide/api' import { Badge, PageHeader, PageTitle, PropertiesTable, Racks24Icon } from '@oxide/ui' import { RouteTabs, Tab } from 'app/components/RouteTabs' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' InventoryPage.loader = async () => { await apiQueryClient.prefetchQuery('rackListV1', { @@ -49,8 +49,8 @@ export function InventoryPage() { - Sleds - Disks + Sleds + Disks ) diff --git a/app/pages/system/SiloPage.tsx b/app/pages/system/SiloPage.tsx index 74a25b15d..b7a92a164 100644 --- a/app/pages/system/SiloPage.tsx +++ b/app/pages/system/SiloPage.tsx @@ -15,7 +15,7 @@ import { } from '@oxide/ui' import { requireSiloParams, useSiloParams } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' const EmptyState = () => ( } title="No identity providers" /> @@ -46,7 +46,10 @@ export function SiloPage() {

Identity providers

- + New provider diff --git a/app/pages/system/SilosPage.tsx b/app/pages/system/SilosPage.tsx index bdb114248..c7634250e 100644 --- a/app/pages/system/SilosPage.tsx +++ b/app/pages/system/SilosPage.tsx @@ -20,7 +20,7 @@ import { } from '@oxide/ui' import { useQuickActions } from 'app/hooks/use-quick-actions' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' const EmptyState = () => ( ( title="No silos" body="You need to create a silo to be able to see it here" buttonText="New silo" - buttonTo={pb.siloNew()} + buttonTo={pb2.siloNew()} /> ) @@ -65,10 +65,10 @@ export default function SilosPage() { useQuickActions( useMemo( () => [ - { value: 'New silo', onSelect: () => navigate(pb.siloNew()) }, + { value: 'New silo', onSelect: () => navigate(pb2.siloNew()) }, ...(silos?.items || []).map((o) => ({ value: o.name, - onSelect: () => navigate(pb.silo({ siloName: o.name })), + onSelect: () => navigate(pb2.silo({ silo: o.name })), navGroup: 'Go to silo', })), ], @@ -82,12 +82,12 @@ export default function SilosPage() { }>Silos - + New silo
} makeActions={makeActions}> - pb.silo({ siloName }))} /> + pb2.silo({ silo }))} /> { const path = requireUpdateParams(params) @@ -28,7 +28,7 @@ export function UpdateDetailSideModal() { path: useUpdateParams(), }) - const dismiss = () => navigate(pb.systemUpdates()) + const dismiss = () => navigate(pb2.systemUpdates()) const startUpdate = useApiMutation('systemUpdateStart', { onSuccess() { diff --git a/app/pages/system/UpdatePage.tsx b/app/pages/system/UpdatePage.tsx index 2745056a8..28e468f10 100644 --- a/app/pages/system/UpdatePage.tsx +++ b/app/pages/system/UpdatePage.tsx @@ -26,7 +26,7 @@ import { import { sortBy } from '@oxide/util' import { RouteTabs, Tab } from 'app/components/RouteTabs' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' UpdatePageUpdates.loader = async () => { await apiQueryClient.prefetchQuery('systemUpdateList', { query: { limit: 10 } }) @@ -46,7 +46,7 @@ export function UpdatePageUpdates() { > pb.systemUpdateDetail({ version }))} + cell={linkCell((version) => pb2.systemUpdateDetail({ version }))} />
@@ -242,9 +242,9 @@ export function UpdatePage() { - Updates - Components - History + Updates + Components + History ) diff --git a/app/routes.tsx b/app/routes.tsx index f7184c691..c67659979 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -60,7 +60,7 @@ import { UpdatePageHistory, UpdatePageUpdates, } from './pages/system/UpdatePage' -import { pb } from './util/path-builder' +import { pb2 } from './util/path-builder' const orgCrumb: CrumbFunc = (m) => m.params.orgName! const projectCrumb: CrumbFunc = (m) => m.params.projectName! @@ -143,7 +143,7 @@ export const routes = createRoutesFromElements( - } /> + } /> {/* These are done here instead of nested so we don't flash a layout on 404s */} } /> diff --git a/app/test/instance-create.e2e.ts b/app/test/instance-create.e2e.ts index 74dcbe71b..193a19f48 100644 --- a/app/test/instance-create.e2e.ts +++ b/app/test/instance-create.e2e.ts @@ -1,7 +1,7 @@ import { globalImages } from '@oxide/api-mocks' import { expectVisible, test } from 'app/test/e2e' -import { pb } from 'app/util/path-builder' +import { pb2 } from 'app/util/path-builder' test.beforeEach(async ({ createProject, orgName, projectName }) => { await createProject(orgName, projectName) @@ -13,7 +13,7 @@ test('can invoke instance create form from instances page', async ({ projectName, genName, }) => { - await page.goto(pb.instances({ orgName, projectName })) + await page.goto(pb2.instances({ organization: orgName, project: projectName })) await page.locator('text="New Instance"').click() await expectVisible(page, [ @@ -42,7 +42,13 @@ test('can invoke instance create form from instances page', async ({ await page.locator('button:has-text("Create instance")').click() - await page.waitForURL(pb.instancePage({ orgName, projectName, instanceName })) + await page.waitForURL( + pb2.instancePage({ + organization: orgName, + project: projectName, + instance: instanceName, + }) + ) await expectVisible(page, [ `h1:has-text("${instanceName}")`, diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 5eaf67cca..7d66ca3b1 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -1,17 +1,17 @@ -import { pb } from './path-builder' +import { pb2 } from './path-builder' // params can be the same for all of them because they only use what they need const params = { - orgName: 'a', - projectName: 'b', - instanceName: 'c', - vpcName: 'd', - siloName: 's', + organization: 'a', + project: 'b', + instance: 'c', + vpc: 'd', + silo: 's', version: 'v', } test('path builder', () => { - expect(Object.fromEntries(Object.entries(pb).map(([key, fn]) => [key, fn(params)]))) + expect(Object.fromEntries(Object.entries(pb2).map(([key, fn]) => [key, fn(params)]))) .toMatchInlineSnapshot(` { "deviceSuccess": "/device/success", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index bcf4f6e94..8cdfe0987 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -1,87 +1,4 @@ -import type { PathParams as PP, PathParamsV1 as PPv1 } from '@oxide/api' - -export const pb = { - orgs: () => '/orgs', - orgNew: () => '/orgs-new', - org: ({ orgName }: PP.Org) => `${pb.orgs()}/${orgName}`, - orgEdit: (params: PP.Org) => `${pb.org(params)}/edit`, - orgAccess: (params: PP.Org) => `${pb.org(params)}/access`, - - projects: (params: PP.Org) => `${pb.org(params)}/projects`, - projectNew: (params: PP.Org) => `${pb.org(params)}/projects-new`, - project: ({ orgName, projectName }: PP.Project) => - `${pb.projects({ orgName })}/${projectName}`, - projectEdit: (params: PP.Project) => `${pb.project(params)}/edit`, - - projectAccess: (params: PP.Project) => `${pb.project(params)}/access`, - projectImages: (params: PP.Project) => `${pb.project(params)}/images`, - - instances: (params: PP.Project) => `${pb.project(params)}/instances`, - instanceNew: (params: PP.Project) => `${pb.project(params)}/instances-new`, - instance: (params: PP.Instance) => `${pb.instances(params)}/${params.instanceName}`, - - /** - * This route exists as a direct link to the default tab of the instance page. Unfortunately - * we don't currently have a good mechanism at the moment to handle a redirect to the default - * tab in a seemless way so we need all in-app links to go directly to the default tab. - * - * @see https://github.com/oxidecomputer/console/pull/1267#discussion_r1016766205 - */ - instancePage: (params: PP.Instance) => pb.instanceStorage(params), - - instanceMetrics: (params: PP.Instance) => `${pb.instance(params)}/metrics`, - instanceStorage: (params: PP.Instance) => `${pb.instance(params)}/storage`, - - nics: (params: PP.Instance) => `${pb.instance(params)}/network-interfaces`, - - serialConsole: (params: PP.Instance) => `${pb.instance(params)}/serial-console`, - - diskNew: (params: PP.Project) => `${pb.project(params)}/disks-new`, - disks: (params: PP.Project) => `${pb.project(params)}/disks`, - - snapshotNew: (params: PP.Project) => `${pb.project(params)}/snapshots-new`, - snapshots: (params: PP.Project) => `${pb.project(params)}/snapshots`, - - vpcNew: (params: PP.Project) => `${pb.project(params)}/vpcs-new`, - vpcs: (params: PP.Project) => `${pb.project(params)}/vpcs`, - vpc: (params: PP.Vpc) => `${pb.vpcs(params)}/${params.vpcName}`, - vpcEdit: (params: PP.Vpc) => `${pb.vpc(params)}/edit`, - - siloUtilization: () => '/utilization', - siloAccess: () => '/access', - - system: () => '/sys', - systemIssues: () => '/sys/issues', - systemUtilization: () => '/sys/utilization', - systemHealth: () => '/sys/health', - - systemUpdates: () => '/sys/update/updates', - systemUpdateDetail: (params: PP.SystemUpdate) => - `${pb.systemUpdates()}/${params.version}`, - systemUpdateHistory: () => '/sys/update/history', - updateableComponents: () => '/sys/update/components', - - systemNetworking: () => '/sys/networking', - systemSettings: () => '/sys/settings', - - rackInventory: () => '/sys/inventory/racks', - sledInventory: () => '/sys/inventory/sleds', - diskInventory: () => '/sys/inventory/disks', - - silos: () => '/sys/silos', - siloNew: () => '/sys/silos-new', - silo: ({ siloName }: PP.Silo) => `/sys/silos/${siloName}`, - siloIdpNew: (params: PP.Silo) => `${pb.silo(params)}/idps-new`, - - settings: () => '/settings', - profile: () => '/settings/profile', - sshKeys: () => '/settings/ssh-keys', - sshKeyNew: () => '/settings/ssh-keys-new', - - deviceSuccess: () => '/device/success', -} - -// export const jelly = 'just kidding' +import type { PathParamsV1 as PPv1 } from '@oxide/api' // TODO: required versions of path params probably belong somewhere else, // they're useful @@ -173,3 +90,5 @@ export const pb2 = { deviceSuccess: () => '/device/success', } + +// export const jelly = 'just kidding' diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index dbe5b31dd..5656468e0 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -148,7 +148,7 @@ export const lookup = { if (!update) throw notFoundErr return update }, - sled(params: PP.Id): Json { + sled(params: PPv1.Id): Json { const sled = db.sleds.find((sled) => sled.id === params.id) if (!sled) throw notFoundErr return sled From bf4d3fdf9c412e5a11d0aa735d2fd2b29bee78c9 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 14:18:28 -0600 Subject: [PATCH 36/39] rename pb2 back to pb --- app/components/TopBar.tsx | 4 +- app/components/TopBarPicker.tsx | 14 ++--- app/forms/idp-create.tsx | 4 +- app/forms/instance-create.tsx | 6 +- app/forms/org-create.tsx | 6 +- app/forms/org-edit.tsx | 4 +- app/forms/project-create.tsx | 6 +- app/forms/project-edit.tsx | 4 +- app/forms/silo-create.tsx | 4 +- app/forms/snapshot-create.tsx | 4 +- app/forms/ssh-key-create.tsx | 4 +- app/forms/vpc-create.tsx | 6 +- app/forms/vpc-edit.tsx | 4 +- app/layouts/OrgLayout.tsx | 8 +-- app/layouts/ProjectLayout.tsx | 16 ++--- app/layouts/SettingsLayout.tsx | 6 +- app/layouts/SiloLayout.tsx | 8 +-- app/layouts/SystemLayout.tsx | 18 +++--- app/pages/DeviceAuthVerifyPage.tsx | 4 +- app/pages/LoginPage.tsx | 4 +- app/pages/OrgsPage.tsx | 14 ++--- app/pages/ProjectsPage.tsx | 14 ++--- app/pages/__tests__/silos.e2e.ts | 4 +- app/pages/__tests__/top-bar.e2e.ts | 4 +- app/pages/project/disks/DisksPage.tsx | 8 +-- app/pages/project/instances/InstancesPage.tsx | 12 ++-- app/pages/project/instances/actions.tsx | 4 +- .../instances/instance/InstancePage.tsx | 12 ++-- .../instances/instance/tabs/NetworkingTab.tsx | 4 +- app/pages/project/networking/VpcsPage.tsx | 12 ++-- app/pages/project/snapshots/SnapshotsPage.tsx | 6 +- app/pages/settings/SSHKeysPage.tsx | 6 +- .../system/InventoryPage/InventoryPage.tsx | 6 +- app/pages/system/SiloPage.tsx | 4 +- app/pages/system/SilosPage.tsx | 12 ++-- app/pages/system/UpdateDetailSideModal.tsx | 4 +- app/pages/system/UpdatePage.tsx | 10 ++-- app/routes.tsx | 4 +- app/test/instance-create.e2e.ts | 6 +- app/util/path-builder.spec.ts | 4 +- app/util/path-builder.ts | 58 +++++++++---------- 41 files changed, 171 insertions(+), 171 deletions(-) diff --git a/app/components/TopBar.tsx b/app/components/TopBar.tsx index a22723133..08bb6b30e 100644 --- a/app/components/TopBar.tsx +++ b/app/components/TopBar.tsx @@ -11,7 +11,7 @@ import { Profile16Icon, } from '@oxide/ui' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' export function TopBar({ children }: { children: React.ReactNode }) { const navigate = useNavigate() @@ -71,7 +71,7 @@ export function TopBar({ children }: { children: React.ReactNode }) { - navigate(pb2.settings())}>Settings + navigate(pb.settings())}>Settings {loggedIn ? ( logout.mutate({})}>Sign out ) : ( diff --git a/app/components/TopBarPicker.tsx b/app/components/TopBarPicker.tsx index 99d5fa49a..5f19aeaf5 100644 --- a/app/components/TopBarPicker.tsx +++ b/app/components/TopBarPicker.tsx @@ -6,7 +6,7 @@ import { useApiQuery } from '@oxide/api' import { Identicon, Organization16Icon, SelectArrows6Icon, Success12Icon } from '@oxide/ui' import { useInstanceSelector, useProjectSelector, useSiloParams } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' type TopBarPickerItem = { label: string @@ -130,8 +130,8 @@ export function useSiloSystemPicker(value: 'silo' | 'system') { export function SiloSystemPicker({ value }: { value: 'silo' | 'system' }) { const commonProps = { items: [ - { label: 'System', to: pb2.silos() }, - { label: 'Silo', to: pb2.orgs() }, + { label: 'System', to: pb.silos() }, + { label: 'Silo', to: pb.orgs() }, ], 'aria-label': 'Switch between system and silo', } @@ -157,7 +157,7 @@ export function SiloPicker() { const { data } = useApiQuery('siloList', { query: { limit: 10 } }) const items = (data?.items || []).map((silo) => ({ label: silo.name, - to: pb2.silo({ silo: silo.name }), + to: pb.silo({ silo: silo.name }), })) return ( @@ -177,7 +177,7 @@ export function OrgPicker() { const { data } = useApiQuery('organizationListV1', { query: { limit: 20 } }) const items = (data?.items || []).map(({ name }) => ({ label: name, - to: pb2.projects({ organization: name }), + to: pb.projects({ organization: name }), })) return ( @@ -200,7 +200,7 @@ export function ProjectPicker() { }) const items = (data?.items || []).map(({ name }) => ({ label: name, - to: pb2.instances({ organization, project: name }), + to: pb.instances({ organization, project: name }), })) return ( @@ -222,7 +222,7 @@ export function InstancePicker() { }) const items = (data?.items || []).map(({ name }) => ({ label: name, - to: pb2.instance({ organization, project, instance: name }), + to: pb.instance({ organization, project, instance: name }), })) return ( diff --git a/app/forms/idp-create.tsx b/app/forms/idp-create.tsx index b2da146d8..243709979 100644 --- a/app/forms/idp-create.tsx +++ b/app/forms/idp-create.tsx @@ -13,7 +13,7 @@ import { TextField, TextFieldInner, } from 'app/components/form' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { useSiloParams, useToast } from '../hooks' @@ -43,7 +43,7 @@ export function CreateIdpSideModalForm() { const { siloName } = useSiloParams() - const onDismiss = () => navigate(pb2.silo({ silo: siloName })) + const onDismiss = () => navigate(pb.silo({ silo: siloName })) const createIdp = useApiMutation('samlIdentityProviderCreate', { onSuccess() { diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 9c51f5f57..c83e6bcf2 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -35,7 +35,7 @@ import { TextField, } from 'app/components/form' import { useProjectSelector, useToast } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' export type InstanceCreateInput = Assign< // API accepts undefined but it's easier if we don't @@ -97,7 +97,7 @@ export function CreateInstanceForm() { title: 'Success!', content: 'Your instance has been created.', }) - navigate(pb2.instancePage({ ...projectSelector, instance: instance.name })) + navigate(pb.instancePage({ ...projectSelector, instance: instance.name })) }, }) @@ -266,7 +266,7 @@ export function CreateInstanceForm() { Create instance - navigate(pb2.instances(projectSelector))} /> + navigate(pb.instances(projectSelector))} /> )} diff --git a/app/forms/org-create.tsx b/app/forms/org-create.tsx index 251bffdaa..fb71549b2 100644 --- a/app/forms/org-create.tsx +++ b/app/forms/org-create.tsx @@ -6,7 +6,7 @@ import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { useToast } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const defaultValues: OrganizationCreate = { name: '', @@ -32,7 +32,7 @@ export function CreateOrgSideModalForm() { title: 'Success!', content: 'Your organization has been created.', }) - navigate(pb2.projects({ organization: org.name })) + navigate(pb.projects({ organization: org.name })) }, }) @@ -41,7 +41,7 @@ export function CreateOrgSideModalForm() { id="create-org-form" formOptions={{ defaultValues }} title="Create organization" - onDismiss={() => navigate(pb2.orgs())} + onDismiss={() => navigate(pb.orgs())} onSubmit={(values) => createOrg.mutate({ body: values })} loading={createOrg.isLoading} submitError={createOrg.error} diff --git a/app/forms/org-edit.tsx b/app/forms/org-edit.tsx index e9c0aaf57..d2a8a6019 100644 --- a/app/forms/org-edit.tsx +++ b/app/forms/org-edit.tsx @@ -6,7 +6,7 @@ import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { requireOrgParams, useOrgSelector, useToast } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' EditOrgSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { const { orgName } = requireOrgParams(params) @@ -23,7 +23,7 @@ export function EditOrgSideModalForm() { const { organization } = useOrgSelector() - const onDismiss = () => navigate(pb2.orgs()) + const onDismiss = () => navigate(pb.orgs()) const { data: org } = useApiQuery('organizationViewV1', { path: { organization } }) diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index 3acd76bdf..bbfabd93a 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -6,7 +6,7 @@ import { Success16Icon } from '@oxide/ui' import { toPathQuery } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { useOrgSelector, useToast } from '../hooks' @@ -22,7 +22,7 @@ export function CreateProjectSideModalForm() { const { organization } = useOrgSelector() - const onDismiss = () => navigate(pb2.projects({ organization })) + const onDismiss = () => navigate(pb.projects({ organization })) const createProject = useApiMutation('projectCreateV1', { onSuccess(project) { @@ -40,7 +40,7 @@ export function CreateProjectSideModalForm() { title: 'Success!', content: 'Your project has been created.', }) - navigate(pb2.instances(projectSelector)) + navigate(pb.instances(projectSelector)) }, }) diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index c7d9e77de..069208b6a 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -6,7 +6,7 @@ import { Success16Icon } from '@oxide/ui' import { toPathQuery } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { getProjectSelector, useProjectSelector, useToast } from '../hooks' @@ -27,7 +27,7 @@ export function EditProjectSideModalForm() { const projectPathQuery = toPathQuery('project', projectSelector) const { organization } = projectSelector - const onDismiss = () => navigate(pb2.projects(projectSelector)) + const onDismiss = () => navigate(pb.projects(projectSelector)) const { data: project } = useApiQuery('projectViewV1', projectPathQuery) diff --git a/app/forms/silo-create.tsx b/app/forms/silo-create.tsx index b5ab8346b..e1058f5ed 100644 --- a/app/forms/silo-create.tsx +++ b/app/forms/silo-create.tsx @@ -12,7 +12,7 @@ import { SideModalForm, } from 'app/components/form' import { useToast } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const defaultValues: SiloCreate = { name: '', @@ -26,7 +26,7 @@ export function CreateSiloSideModalForm() { const queryClient = useApiQueryClient() const addToast = useToast() - const onDismiss = () => navigate(pb2.silos()) + const onDismiss = () => navigate(pb.silos()) const createSilo = useApiMutation('siloCreate', { onSuccess(silo) { diff --git a/app/forms/snapshot-create.tsx b/app/forms/snapshot-create.tsx index cf9693dd2..ad1ff77d0 100644 --- a/app/forms/snapshot-create.tsx +++ b/app/forms/snapshot-create.tsx @@ -11,7 +11,7 @@ import { SideModalForm, } from 'app/components/form' import { useProjectSelector, useToast } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const useSnapshotDiskItems = (projectSelector: PathParamsV1.Project) => { const { data: disks } = useApiQuery('diskListV1', { @@ -38,7 +38,7 @@ export function CreateSnapshotSideModalForm() { const diskItems = useSnapshotDiskItems(projectSelector) - const onDismiss = () => navigate(pb2.snapshots(projectSelector)) + const onDismiss = () => navigate(pb.snapshots(projectSelector)) const createSnapshot = useApiMutation('snapshotCreateV1', { onSuccess() { diff --git a/app/forms/ssh-key-create.tsx b/app/forms/ssh-key-create.tsx index 8fe6d188e..bbb837900 100644 --- a/app/forms/ssh-key-create.tsx +++ b/app/forms/ssh-key-create.tsx @@ -4,7 +4,7 @@ import type { SshKeyCreate } from '@oxide/api' import { useApiMutation, useApiQueryClient } from '@oxide/api' import { DescriptionField, NameField, SideModalForm, TextField } from 'app/components/form' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const defaultValues: SshKeyCreate = { name: '', @@ -16,7 +16,7 @@ export function CreateSSHKeySideModalForm() { const queryClient = useApiQueryClient() const navigate = useNavigate() - const onDismiss = () => navigate(pb2.sshKeys()) + const onDismiss = () => navigate(pb.sshKeys()) const createSshKey = useApiMutation('sessionSshkeyCreate', { onSuccess() { diff --git a/app/forms/vpc-create.tsx b/app/forms/vpc-create.tsx index c9d7d1f33..209aaf630 100644 --- a/app/forms/vpc-create.tsx +++ b/app/forms/vpc-create.tsx @@ -7,7 +7,7 @@ import { toPathQuery } from '@oxide/util' import { DescriptionField, NameField, SideModalForm, TextField } from 'app/components/form' import { useProjectSelector, useToast } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const defaultValues: VpcCreate = { name: '', @@ -32,7 +32,7 @@ export function CreateVpcSideModalForm() { title: 'Success!', content: 'Your VPC has been created.', }) - navigate(pb2.vpc(vpcSelector)) + navigate(pb.vpc(vpcSelector)) }, }) @@ -42,7 +42,7 @@ export function CreateVpcSideModalForm() { title="Create VPC" formOptions={{ defaultValues }} onSubmit={(values) => createVpc.mutate({ query: projectSelector, body: values })} - onDismiss={() => navigate(pb2.vpcs(projectSelector))} + onDismiss={() => navigate(pb.vpcs(projectSelector))} loading={createVpc.isLoading} submitError={createVpc.error} > diff --git a/app/forms/vpc-edit.tsx b/app/forms/vpc-edit.tsx index db2ad6235..5f12131c5 100644 --- a/app/forms/vpc-edit.tsx +++ b/app/forms/vpc-edit.tsx @@ -7,7 +7,7 @@ import { pick, toPathQuery } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { getVpcSelector, useToast, useVpcSelector } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' EditVpcSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { await apiQueryClient.prefetchQuery( @@ -27,7 +27,7 @@ export function EditVpcSideModalForm() { const { data: vpc } = useApiQuery('vpcViewV1', vpcPathQuery) - const onDismiss = () => navigate(pb2.vpcs(projectSelector)) + const onDismiss = () => navigate(pb.vpcs(projectSelector)) const editVpc = useApiMutation('vpcUpdateV1', { async onSuccess(vpc) { diff --git a/app/layouts/OrgLayout.tsx b/app/layouts/OrgLayout.tsx index 24dbebb7a..099710702 100644 --- a/app/layouts/OrgLayout.tsx +++ b/app/layouts/OrgLayout.tsx @@ -3,7 +3,7 @@ import { Access16Icon, Divider, Folder16Icon, Organization16Icon } from '@oxide/ import { TopBar } from 'app/components/TopBar' import { OrgPicker, useSiloSystemPicker } from 'app/components/TopBarPicker' import { useOrgSelector } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { DocsLinkItem, NavLinkItem, Sidebar } from '../components/Sidebar' import { ContentPane, PageContainer } from './helpers' @@ -19,7 +19,7 @@ const OrgLayout = () => { - + Organizations @@ -27,11 +27,11 @@ const OrgLayout = () => { - + Projects - + Access & IAM diff --git a/app/layouts/ProjectLayout.tsx b/app/layouts/ProjectLayout.tsx index a27334f33..0501993b3 100644 --- a/app/layouts/ProjectLayout.tsx +++ b/app/layouts/ProjectLayout.tsx @@ -20,7 +20,7 @@ import { useSiloSystemPicker, } from 'app/components/TopBarPicker' import { useProjectSelector, useQuickActions } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { DocsLinkItem, NavLinkItem, Sidebar } from '../components/Sidebar' import { ContentPane, PageContainer } from './helpers' @@ -65,7 +65,7 @@ const ProjectLayout = () => { - + Projects @@ -73,22 +73,22 @@ const ProjectLayout = () => { - + Instances - + Snapshots - + Disks - + Access & IAM - + Images - + Networking diff --git a/app/layouts/SettingsLayout.tsx b/app/layouts/SettingsLayout.tsx index 37a0bc75d..640027331 100644 --- a/app/layouts/SettingsLayout.tsx +++ b/app/layouts/SettingsLayout.tsx @@ -6,7 +6,7 @@ import { Divider, Key16Icon, Profile16Icon } from '@oxide/ui' import { TopBar } from 'app/components/TopBar' import { OrgPicker, useSiloSystemPicker } from 'app/components/TopBarPicker' import { useQuickActions } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { DocsLinkItem, NavLinkItem, Sidebar } from '../components/Sidebar' import { ContentPane, PageContainer } from './helpers' @@ -47,10 +47,10 @@ const SettingsLayout = () => { - + Profile - + SSH Keys diff --git a/app/layouts/SiloLayout.tsx b/app/layouts/SiloLayout.tsx index 9e6483903..cc07e4565 100644 --- a/app/layouts/SiloLayout.tsx +++ b/app/layouts/SiloLayout.tsx @@ -3,7 +3,7 @@ import { Access16Icon, Divider, Organization16Icon, Snapshots16Icon } from '@oxi import { DocsLinkItem, NavLinkItem, Sidebar } from 'app/components/Sidebar' import { TopBar } from 'app/components/TopBar' import { OrgPicker, useSiloSystemPicker } from 'app/components/TopBarPicker' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { ContentPane, PageContainer } from './helpers' @@ -21,13 +21,13 @@ export default function SiloLayout() { {/* TODO: silo name in heading */} - + Organizations - + Utilization - + Access & IAM diff --git a/app/layouts/SystemLayout.tsx b/app/layouts/SystemLayout.tsx index d2d64070d..db375335b 100644 --- a/app/layouts/SystemLayout.tsx +++ b/app/layouts/SystemLayout.tsx @@ -17,7 +17,7 @@ import { trigger404 } from 'app/components/ErrorBoundary' import { DocsLinkItem, NavLinkItem, Sidebar } from 'app/components/Sidebar' import { TopBar } from 'app/components/TopBar' import { SiloPicker, SiloSystemPicker } from 'app/components/TopBarPicker' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { ContentPane, PageContainer } from './helpers' @@ -59,28 +59,28 @@ export default function SystemLayout() { - + Silos - + Issues - + Utilization - + Inventory - + Health - + System Update - + Networking - + Settings diff --git a/app/pages/DeviceAuthVerifyPage.tsx b/app/pages/DeviceAuthVerifyPage.tsx index 65db3fcb1..d0166519b 100644 --- a/app/pages/DeviceAuthVerifyPage.tsx +++ b/app/pages/DeviceAuthVerifyPage.tsx @@ -3,7 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom' import { useApiMutation } from '@oxide/api' import { Button, Warning12Icon } from '@oxide/ui' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { useToast } from '../hooks' @@ -16,7 +16,7 @@ export default function DeviceAuthVerifyPage() { const addToast = useToast() const confirmPost = useApiMutation('deviceAuthConfirm', { onSuccess: () => { - navigate(pb2.deviceSuccess()) + navigate(pb.deviceSuccess()) }, onError: () => { addToast({ diff --git a/app/pages/LoginPage.tsx b/app/pages/LoginPage.tsx index 90371f268..12b78058b 100644 --- a/app/pages/LoginPage.tsx +++ b/app/pages/LoginPage.tsx @@ -3,7 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom' import { useApiMutation } from '@oxide/api' import { Button, Success16Icon, Warning12Icon } from '@oxide/ui' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { useToast } from '../hooks' @@ -30,7 +30,7 @@ export default function LoginPage() { title: 'Logged in', icon: , }) - navigate(searchParams.get('state') || pb2.orgs()) + navigate(searchParams.get('state') || pb.orgs()) }, onError: () => { addToast({ diff --git a/app/pages/OrgsPage.tsx b/app/pages/OrgsPage.tsx index 7f34b604b..e1eb99e41 100644 --- a/app/pages/OrgsPage.tsx +++ b/app/pages/OrgsPage.tsx @@ -16,7 +16,7 @@ import { buttonStyle, } from '@oxide/ui' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { useQuickActions } from '../hooks' @@ -26,7 +26,7 @@ const EmptyState = () => ( title="No organizations" body="You need to create an organization to be able to see it here" buttonText="New organization" - buttonTo={pb2.orgNew()} + buttonTo={pb.orgNew()} /> ) @@ -60,7 +60,7 @@ export default function OrgsPage() { { path: { organization: org.name } }, org ) - navigate(pb2.orgEdit({ organization: org.name })) + navigate(pb.orgEdit({ organization: org.name })) }, }, { @@ -74,10 +74,10 @@ export default function OrgsPage() { useQuickActions( useMemo( () => [ - { value: 'New organization', onSelect: () => navigate(pb2.orgNew()) }, + { value: 'New organization', onSelect: () => navigate(pb.orgNew()) }, ...(orgs?.items || []).map((o) => ({ value: o.name, - onSelect: () => navigate(pb2.org({ organization: o.name })), + onSelect: () => navigate(pb.org({ organization: o.name })), navGroup: 'Go to organization', })), ], @@ -91,14 +91,14 @@ export default function OrgsPage() { }>Organizations - + New Organization } makeActions={makeActions}> pb2.projects({ organization }))} + cell={linkCell((organization) => pb.projects({ organization }))} /> diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index 88d32ec8e..820764a4d 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -16,7 +16,7 @@ import { buttonStyle, } from '@oxide/ui' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { getOrgSelector, useOrgSelector, useQuickActions } from '../hooks' @@ -26,7 +26,7 @@ const EmptyState = () => ( title="No projects" body="You need to create a project to be able to see it here" buttonText="New project" - buttonTo={pb2.projectNew(useOrgSelector())} + buttonTo={pb.projectNew(useOrgSelector())} /> ) @@ -71,7 +71,7 @@ export default function ProjectsPage() { }, project ) - navigate(pb2.projectEdit({ organization, project: project.name })) + navigate(pb.projectEdit({ organization, project: project.name })) }, }, { @@ -90,11 +90,11 @@ export default function ProjectsPage() { () => [ { value: 'New project', - onSelect: () => navigate(pb2.projectNew({ organization })), + onSelect: () => navigate(pb.projectNew({ organization })), }, ...(projects?.items || []).map((p) => ({ value: p.name, - onSelect: () => navigate(pb2.instances({ organization, project: p.name })), + onSelect: () => navigate(pb.instances({ organization, project: p.name })), navGroup: 'Go to project', })), ], @@ -108,14 +108,14 @@ export default function ProjectsPage() { }>Projects - + New Project
} makeActions={makeActions}> pb2.instances({ organization, project }))} + cell={linkCell((project) => pb.instances({ organization, project }))} /> diff --git a/app/pages/__tests__/silos.e2e.ts b/app/pages/__tests__/silos.e2e.ts index cf1e8675b..87a69bdaa 100644 --- a/app/pages/__tests__/silos.e2e.ts +++ b/app/pages/__tests__/silos.e2e.ts @@ -1,10 +1,10 @@ import { expect, test } from '@playwright/test' import { expectNotVisible, expectRowVisible, expectVisible } from 'app/test/e2e' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' test('Silos page', async ({ page }) => { - await page.goto(pb2.silos()) + await page.goto(pb.silos()) await expectVisible(page, ['role=heading[name*="Silos"]']) const table = page.locator('role=table') diff --git a/app/pages/__tests__/top-bar.e2e.ts b/app/pages/__tests__/top-bar.e2e.ts index 1b3db6c60..b97d6daad 100644 --- a/app/pages/__tests__/top-bar.e2e.ts +++ b/app/pages/__tests__/top-bar.e2e.ts @@ -1,10 +1,10 @@ import { test } from '@playwright/test' import { expectSimultaneous } from 'app/test/e2e' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' test('Silo/system picker does not pop in', async ({ page }) => { - await page.goto(pb2.orgs()) + await page.goto(pb.orgs()) // make sure the system policy call is prefetched properly so that the // silo/system picker doesn't pop in. if this turns out to be flaky, just diff --git a/app/pages/project/disks/DisksPage.tsx b/app/pages/project/disks/DisksPage.tsx index d792396c4..49d885cb4 100644 --- a/app/pages/project/disks/DisksPage.tsx +++ b/app/pages/project/disks/DisksPage.tsx @@ -23,7 +23,7 @@ import { import { DiskStatusBadge } from 'app/components/StatusBadge' import { getProjectSelector, useProjectSelector, useToast } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' function AttachedInstance({ instanceId, @@ -39,7 +39,7 @@ function AttachedInstance({ return instance ? ( {instance.name} @@ -52,7 +52,7 @@ const EmptyState = () => ( title="No disks" body="You need to create a disk to be able to see it here" buttonText="New disk" - buttonTo={pb2.diskNew(useProjectSelector())} + buttonTo={pb.diskNew(useProjectSelector())} /> ) @@ -122,7 +122,7 @@ export function DisksPage() { }>Disks - + New Disk diff --git a/app/pages/project/instances/InstancesPage.tsx b/app/pages/project/instances/InstancesPage.tsx index e988a57ad..dd16ac47f 100644 --- a/app/pages/project/instances/InstancesPage.tsx +++ b/app/pages/project/instances/InstancesPage.tsx @@ -22,7 +22,7 @@ import { } from '@oxide/ui' import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { useMakeInstanceActions } from './actions' @@ -32,7 +32,7 @@ const EmptyState = () => ( title="No instances" body="You need to create an instance to be able to see it here" buttonText="New instance" - buttonTo={pb2.instanceNew(useProjectSelector())} + buttonTo={pb.instanceNew(useProjectSelector())} /> ) @@ -64,12 +64,12 @@ export function InstancesPage() { () => [ { value: 'New instance', - onSelect: () => navigate(pb2.instanceNew(projectSelector)), + onSelect: () => navigate(pb.instanceNew(projectSelector)), }, ...(instances?.items || []).map((i) => ({ value: i.name, onSelect: () => - navigate(pb2.instancePage({ ...projectSelector, instance: i.name })), + navigate(pb.instancePage({ ...projectSelector, instance: i.name })), navGroup: 'Go to instance', })), ], @@ -94,14 +94,14 @@ export function InstancesPage() { - + New Instance
}> pb2.instancePage({ ...projectSelector, instance }))} + cell={linkCell((instance) => pb.instancePage({ ...projectSelector, instance }))} /> boolean> = { start: (i) => i.runState === 'stopped', @@ -76,7 +76,7 @@ export const useMakeInstanceActions = ( { label: 'View serial console', onActivate() { - navigate(pb2.serialConsole(instanceSelector)) + navigate(pb.serialConsole(instanceSelector)) }, }, { diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index b5ed203bf..2312fe5bd 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -11,7 +11,7 @@ import { MoreActionsMenu } from 'app/components/MoreActionsMenu' import { RouteTabs, Tab } from 'app/components/RouteTabs' import { InstanceStatusBadge } from 'app/components/StatusBadge' import { getInstanceSelector, useInstanceSelector, useQuickActions } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' import { useMakeInstanceActions } from '../actions' @@ -38,7 +38,7 @@ export function InstancePage() { queryClient.invalidateQueries('instanceViewV1', instancePathQuery) }, // go to project instances list since there's no more instance - onDelete: () => navigate(pb2.instances(instanceSelector)), + onDelete: () => navigate(pb.instances(instanceSelector)), } ) @@ -91,10 +91,10 @@ export function InstancePage() { - Storage - Metrics - Network Interfaces - Serial Console + Storage + Metrics + Network Interfaces + Serial Console ) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 30e982011..5a630c189 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -26,7 +26,7 @@ import { useRequiredParams, useToast, } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const VpcNameFromId = ({ value }: { value: string }) => { const projectSelector = useProjectSelector() @@ -35,7 +35,7 @@ const VpcNameFromId = ({ value }: { value: string }) => { return ( {vpc.name} diff --git a/app/pages/project/networking/VpcsPage.tsx b/app/pages/project/networking/VpcsPage.tsx index 24e668075..6529ee61f 100644 --- a/app/pages/project/networking/VpcsPage.tsx +++ b/app/pages/project/networking/VpcsPage.tsx @@ -17,7 +17,7 @@ import { } from '@oxide/ui' import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const EmptyState = () => ( ( title="No VPCs" body="You need to create a VPC to be able to see it here" buttonText="New VPC" - buttonTo={pb2.vpcNew(useProjectSelector())} + buttonTo={pb.vpcNew(useProjectSelector())} /> ) @@ -56,7 +56,7 @@ export function VpcsPage() { { label: 'Edit', onActivate() { - navigate(pb2.vpcEdit({ ...projectSelector, vpc: vpc.name }), { state: vpc }) + navigate(pb.vpcEdit({ ...projectSelector, vpc: vpc.name }), { state: vpc }) }, }, { @@ -72,7 +72,7 @@ export function VpcsPage() { () => (vpcs?.items || []).map((v) => ({ value: v.name, - onSelect: () => navigate(pb2.vpc({ ...projectSelector, vpc: v.name })), + onSelect: () => navigate(pb.vpc({ ...projectSelector, vpc: v.name })), navGroup: 'Go to VPC', })), [projectSelector, vpcs, navigate] @@ -86,14 +86,14 @@ export function VpcsPage() { }>VPCs - + New Vpc
} makeActions={makeActions}> pb2.vpc({ ...projectSelector, vpc }))} + cell={linkCell((vpc) => pb.vpc({ ...projectSelector, vpc }))} /> diff --git a/app/pages/project/snapshots/SnapshotsPage.tsx b/app/pages/project/snapshots/SnapshotsPage.tsx index 01ead7849..380a2eb91 100644 --- a/app/pages/project/snapshots/SnapshotsPage.tsx +++ b/app/pages/project/snapshots/SnapshotsPage.tsx @@ -16,7 +16,7 @@ import { import { SnapshotStatusBadge } from 'app/components/StatusBadge' import { getProjectSelector, useProjectSelector } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const DiskNameFromId = ({ value }: { value: string }) => { const { data: disk } = useApiQuery('diskViewV1', { path: { disk: value } }) @@ -30,7 +30,7 @@ const EmptyState = () => ( title="No snapshots" body="You need to create a snapshot to be able to see it here" buttonText="New snapshot" - buttonTo={pb2.snapshotNew(useProjectSelector())} + buttonTo={pb.snapshotNew(useProjectSelector())} /> ) @@ -67,7 +67,7 @@ export function SnapshotsPage() { }>Snapshots - + New Snapshot diff --git a/app/pages/settings/SSHKeysPage.tsx b/app/pages/settings/SSHKeysPage.tsx index 09ac7c05a..3e88bd812 100644 --- a/app/pages/settings/SSHKeysPage.tsx +++ b/app/pages/settings/SSHKeysPage.tsx @@ -16,7 +16,7 @@ import { buttonStyle, } from '@oxide/ui' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' SSHKeysPage.loader = async () => { await apiQueryClient.prefetchQuery('sessionSshkeyList', { query: { limit: 10 } }) @@ -53,7 +53,7 @@ export function SSHKeysPage() { }>SSH Keys - + Add SSH key @@ -65,7 +65,7 @@ export function SSHKeysPage() { title="No SSH keys" body="You need to create an ssh key to be able to see it here" buttonText="Add SSH key" - onClick={() => navigate(pb2.sshKeyNew())} + onClick={() => navigate(pb.sshKeyNew())} /> } > diff --git a/app/pages/system/InventoryPage/InventoryPage.tsx b/app/pages/system/InventoryPage/InventoryPage.tsx index cd97292d7..2a170d790 100644 --- a/app/pages/system/InventoryPage/InventoryPage.tsx +++ b/app/pages/system/InventoryPage/InventoryPage.tsx @@ -4,7 +4,7 @@ import { apiQueryClient, useApiQuery } from '@oxide/api' import { Badge, PageHeader, PageTitle, PropertiesTable, Racks24Icon } from '@oxide/ui' import { RouteTabs, Tab } from 'app/components/RouteTabs' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' InventoryPage.loader = async () => { await apiQueryClient.prefetchQuery('rackListV1', { @@ -49,8 +49,8 @@ export function InventoryPage() { - Sleds - Disks + Sleds + Disks ) diff --git a/app/pages/system/SiloPage.tsx b/app/pages/system/SiloPage.tsx index b7a92a164..d5b1dfb1d 100644 --- a/app/pages/system/SiloPage.tsx +++ b/app/pages/system/SiloPage.tsx @@ -15,7 +15,7 @@ import { } from '@oxide/ui' import { requireSiloParams, useSiloParams } from 'app/hooks' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const EmptyState = () => ( } title="No identity providers" /> @@ -47,7 +47,7 @@ export function SiloPage() {

Identity providers

New provider diff --git a/app/pages/system/SilosPage.tsx b/app/pages/system/SilosPage.tsx index c7634250e..acc947b0b 100644 --- a/app/pages/system/SilosPage.tsx +++ b/app/pages/system/SilosPage.tsx @@ -20,7 +20,7 @@ import { } from '@oxide/ui' import { useQuickActions } from 'app/hooks/use-quick-actions' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' const EmptyState = () => ( ( title="No silos" body="You need to create a silo to be able to see it here" buttonText="New silo" - buttonTo={pb2.siloNew()} + buttonTo={pb.siloNew()} /> ) @@ -65,10 +65,10 @@ export default function SilosPage() { useQuickActions( useMemo( () => [ - { value: 'New silo', onSelect: () => navigate(pb2.siloNew()) }, + { value: 'New silo', onSelect: () => navigate(pb.siloNew()) }, ...(silos?.items || []).map((o) => ({ value: o.name, - onSelect: () => navigate(pb2.silo({ silo: o.name })), + onSelect: () => navigate(pb.silo({ silo: o.name })), navGroup: 'Go to silo', })), ], @@ -82,12 +82,12 @@ export default function SilosPage() { }>Silos - + New silo
} makeActions={makeActions}> - pb2.silo({ silo }))} /> + pb.silo({ silo }))} /> { const path = requireUpdateParams(params) @@ -28,7 +28,7 @@ export function UpdateDetailSideModal() { path: useUpdateParams(), }) - const dismiss = () => navigate(pb2.systemUpdates()) + const dismiss = () => navigate(pb.systemUpdates()) const startUpdate = useApiMutation('systemUpdateStart', { onSuccess() { diff --git a/app/pages/system/UpdatePage.tsx b/app/pages/system/UpdatePage.tsx index 28e468f10..2745056a8 100644 --- a/app/pages/system/UpdatePage.tsx +++ b/app/pages/system/UpdatePage.tsx @@ -26,7 +26,7 @@ import { import { sortBy } from '@oxide/util' import { RouteTabs, Tab } from 'app/components/RouteTabs' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' UpdatePageUpdates.loader = async () => { await apiQueryClient.prefetchQuery('systemUpdateList', { query: { limit: 10 } }) @@ -46,7 +46,7 @@ export function UpdatePageUpdates() { > pb2.systemUpdateDetail({ version }))} + cell={linkCell((version) => pb.systemUpdateDetail({ version }))} />
@@ -242,9 +242,9 @@ export function UpdatePage() { - Updates - Components - History + Updates + Components + History ) diff --git a/app/routes.tsx b/app/routes.tsx index c67659979..f7184c691 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -60,7 +60,7 @@ import { UpdatePageHistory, UpdatePageUpdates, } from './pages/system/UpdatePage' -import { pb2 } from './util/path-builder' +import { pb } from './util/path-builder' const orgCrumb: CrumbFunc = (m) => m.params.orgName! const projectCrumb: CrumbFunc = (m) => m.params.projectName! @@ -143,7 +143,7 @@ export const routes = createRoutesFromElements( - } /> + } /> {/* These are done here instead of nested so we don't flash a layout on 404s */} } /> diff --git a/app/test/instance-create.e2e.ts b/app/test/instance-create.e2e.ts index 193a19f48..2ab5e7494 100644 --- a/app/test/instance-create.e2e.ts +++ b/app/test/instance-create.e2e.ts @@ -1,7 +1,7 @@ import { globalImages } from '@oxide/api-mocks' import { expectVisible, test } from 'app/test/e2e' -import { pb2 } from 'app/util/path-builder' +import { pb } from 'app/util/path-builder' test.beforeEach(async ({ createProject, orgName, projectName }) => { await createProject(orgName, projectName) @@ -13,7 +13,7 @@ test('can invoke instance create form from instances page', async ({ projectName, genName, }) => { - await page.goto(pb2.instances({ organization: orgName, project: projectName })) + await page.goto(pb.instances({ organization: orgName, project: projectName })) await page.locator('text="New Instance"').click() await expectVisible(page, [ @@ -43,7 +43,7 @@ test('can invoke instance create form from instances page', async ({ await page.locator('button:has-text("Create instance")').click() await page.waitForURL( - pb2.instancePage({ + pb.instancePage({ organization: orgName, project: projectName, instance: instanceName, diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 7d66ca3b1..83749d718 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -1,4 +1,4 @@ -import { pb2 } from './path-builder' +import { pb } from './path-builder' // params can be the same for all of them because they only use what they need const params = { @@ -11,7 +11,7 @@ const params = { } test('path builder', () => { - expect(Object.fromEntries(Object.entries(pb2).map(([key, fn]) => [key, fn(params)]))) + expect(Object.fromEntries(Object.entries(pb).map(([key, fn]) => [key, fn(params)]))) .toMatchInlineSnapshot(` { "deviceSuccess": "/device/success", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 8cdfe0987..353026e7f 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -10,26 +10,26 @@ type Vpc = Required type SystemUpdate = Required type Silo = Required -// TODO: obviously the plan is pb2 becomes pb -export const pb2 = { +// TODO: obviously the plan is pb becomes pb +export const pb = { orgs: () => '/orgs', orgNew: () => '/orgs-new', - org: ({ organization }: Org) => `${pb2.orgs()}/${organization}`, - orgEdit: (params: Org) => `${pb2.org(params)}/edit`, - orgAccess: (params: Org) => `${pb2.org(params)}/access`, + org: ({ organization }: Org) => `${pb.orgs()}/${organization}`, + orgEdit: (params: Org) => `${pb.org(params)}/edit`, + orgAccess: (params: Org) => `${pb.org(params)}/access`, - projects: (params: Org) => `${pb2.org(params)}/projects`, - projectNew: (params: Org) => `${pb2.org(params)}/projects-new`, + projects: (params: Org) => `${pb.org(params)}/projects`, + projectNew: (params: Org) => `${pb.org(params)}/projects-new`, project: ({ organization, project }: Project) => - `${pb2.projects({ organization })}/${project}`, - projectEdit: (params: Project) => `${pb2.project(params)}/edit`, + `${pb.projects({ organization })}/${project}`, + projectEdit: (params: Project) => `${pb.project(params)}/edit`, - projectAccess: (params: Project) => `${pb2.project(params)}/access`, - projectImages: (params: Project) => `${pb2.project(params)}/images`, + projectAccess: (params: Project) => `${pb.project(params)}/access`, + projectImages: (params: Project) => `${pb.project(params)}/images`, - instances: (params: Project) => `${pb2.project(params)}/instances`, - instanceNew: (params: Project) => `${pb2.project(params)}/instances-new`, - instance: (params: Instance) => `${pb2.instances(params)}/${params.instance}`, + instances: (params: Project) => `${pb.project(params)}/instances`, + instanceNew: (params: Project) => `${pb.project(params)}/instances-new`, + instance: (params: Instance) => `${pb.instances(params)}/${params.instance}`, /** * This route exists as a direct link to the default tab of the instance page. Unfortunately @@ -38,25 +38,25 @@ export const pb2 = { * * @see https://github.com/oxidecomputer/console/pull/1267#discussion_r1016766205 */ - instancePage: (params: Instance) => pb2.instanceStorage(params), + instancePage: (params: Instance) => pb.instanceStorage(params), - instanceMetrics: (params: Instance) => `${pb2.instance(params)}/metrics`, - instanceStorage: (params: Instance) => `${pb2.instance(params)}/storage`, + instanceMetrics: (params: Instance) => `${pb.instance(params)}/metrics`, + instanceStorage: (params: Instance) => `${pb.instance(params)}/storage`, - nics: (params: Instance) => `${pb2.instance(params)}/network-interfaces`, + nics: (params: Instance) => `${pb.instance(params)}/network-interfaces`, - serialConsole: (params: Instance) => `${pb2.instance(params)}/serial-console`, + serialConsole: (params: Instance) => `${pb.instance(params)}/serial-console`, - diskNew: (params: Project) => `${pb2.project(params)}/disks-new`, - disks: (params: Project) => `${pb2.project(params)}/disks`, + diskNew: (params: Project) => `${pb.project(params)}/disks-new`, + disks: (params: Project) => `${pb.project(params)}/disks`, - snapshotNew: (params: Project) => `${pb2.project(params)}/snapshots-new`, - snapshots: (params: Project) => `${pb2.project(params)}/snapshots`, + snapshotNew: (params: Project) => `${pb.project(params)}/snapshots-new`, + snapshots: (params: Project) => `${pb.project(params)}/snapshots`, - vpcNew: (params: Project) => `${pb2.project(params)}/vpcs-new`, - vpcs: (params: Project) => `${pb2.project(params)}/vpcs`, - vpc: (params: Vpc) => `${pb2.vpcs(params)}/${params.vpc}`, - vpcEdit: (params: Vpc) => `${pb2.vpc(params)}/edit`, + vpcNew: (params: Project) => `${pb.project(params)}/vpcs-new`, + vpcs: (params: Project) => `${pb.project(params)}/vpcs`, + vpc: (params: Vpc) => `${pb.vpcs(params)}/${params.vpc}`, + vpcEdit: (params: Vpc) => `${pb.vpc(params)}/edit`, siloUtilization: () => '/utilization', siloAccess: () => '/access', @@ -67,7 +67,7 @@ export const pb2 = { systemHealth: () => '/sys/health', systemUpdates: () => '/sys/update/updates', - systemUpdateDetail: ({ version }: SystemUpdate) => `${pb2.systemUpdates()}/${version}`, + systemUpdateDetail: ({ version }: SystemUpdate) => `${pb.systemUpdates()}/${version}`, systemUpdateHistory: () => '/sys/update/history', updateableComponents: () => '/sys/update/components', @@ -81,7 +81,7 @@ export const pb2 = { silos: () => '/sys/silos', siloNew: () => '/sys/silos-new', silo: ({ silo }: Silo) => `/sys/silos/${silo}`, - siloIdpNew: (params: Silo) => `${pb2.silo(params)}/idps-new`, + siloIdpNew: (params: Silo) => `${pb.silo(params)}/idps-new`, settings: () => '/settings', profile: () => '/settings/profile', From 18e00837a9b23a1e04ea7f136dbb1eb2cdbe2af5 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 14:23:54 -0600 Subject: [PATCH 37/39] more cleanup --- app/forms/org-edit.tsx | 5 ++--- app/hooks/use-params.ts | 4 ++-- app/pages/project/instances/instance/InstancePage.tsx | 1 - app/util/path-builder.ts | 1 - 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/forms/org-edit.tsx b/app/forms/org-edit.tsx index d2a8a6019..78aab6c98 100644 --- a/app/forms/org-edit.tsx +++ b/app/forms/org-edit.tsx @@ -5,13 +5,12 @@ import { apiQueryClient, useApiMutation, useApiQuery, useApiQueryClient } from ' import { Success16Icon } from '@oxide/ui' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { requireOrgParams, useOrgSelector, useToast } from 'app/hooks' +import { getOrgSelector, useOrgSelector, useToast } from 'app/hooks' import { pb } from 'app/util/path-builder' EditOrgSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { - const { orgName } = requireOrgParams(params) await apiQueryClient.prefetchQuery('organizationViewV1', { - path: { organization: orgName }, + path: getOrgSelector(params), }) return null } diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index fd3191fd1..6221763fd 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -21,9 +21,9 @@ export const requireParams = return requiredParams as { readonly [k in K]: string } } -export const requireOrgParams = requireParams('orgName') +const requireOrgParams = requireParams('orgName') export const requireProjectParams = requireParams('orgName', 'projectName') -export const requireInstanceParams = requireParams('orgName', 'projectName', 'instanceName') +const requireInstanceParams = requireParams('orgName', 'projectName', 'instanceName') const requireVpcParams = requireParams('orgName', 'projectName', 'vpcName') export const requireSiloParams = requireParams('siloName') export const requireSledParams = requireParams('sledId') diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index 2312fe5bd..95ab3c0c0 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -30,7 +30,6 @@ export function InstancePage() { const navigate = useNavigate() const queryClient = useApiQueryClient() - // TODO: change the interface here to take projectSelector directly const makeActions = useMakeInstanceActions( { project, organization }, { diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 353026e7f..7cc616a0a 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -10,7 +10,6 @@ type Vpc = Required type SystemUpdate = Required type Silo = Required -// TODO: obviously the plan is pb becomes pb export const pb = { orgs: () => '/orgs', orgNew: () => '/orgs-new', From 3f5fbd3289b0474120b8630fb19fa6dbfd7f8e97 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 14:32:30 -0600 Subject: [PATCH 38/39] combine path params back into one file --- app/forms/snapshot-create.tsx | 4 ++-- app/util/path-builder.ts | 14 +++++++------- libs/api-mocks/msw/db.ts | 29 +++++++++++++--------------- libs/api/index.ts | 1 - libs/api/path-params-v1.ts | 18 ------------------ libs/api/path-params.ts | 36 ++++++++++++++++++----------------- 6 files changed, 41 insertions(+), 61 deletions(-) delete mode 100644 libs/api/path-params-v1.ts diff --git a/app/forms/snapshot-create.tsx b/app/forms/snapshot-create.tsx index ad1ff77d0..0f186305c 100644 --- a/app/forms/snapshot-create.tsx +++ b/app/forms/snapshot-create.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom' -import type { PathParamsV1, SnapshotCreate } from '@oxide/api' +import type { PathParams as PP, SnapshotCreate } from '@oxide/api' import { useApiMutation, useApiQuery, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' @@ -13,7 +13,7 @@ import { import { useProjectSelector, useToast } from 'app/hooks' import { pb } from 'app/util/path-builder' -const useSnapshotDiskItems = (projectSelector: PathParamsV1.Project) => { +const useSnapshotDiskItems = (projectSelector: PP.Project) => { const { data: disks } = useApiQuery('diskListV1', { query: { ...projectSelector, limit: 1000 }, }) diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 7cc616a0a..64e413e0c 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -1,14 +1,14 @@ -import type { PathParamsV1 as PPv1 } from '@oxide/api' +import type { PathParams as PP } from '@oxide/api' // TODO: required versions of path params probably belong somewhere else, // they're useful -type Org = Required -type Project = Required -type Instance = Required -type Vpc = Required -type SystemUpdate = Required -type Silo = Required +type Org = Required +type Project = Required +type Instance = Required +type Vpc = Required +type SystemUpdate = Required +type Silo = Required export const pb = { orgs: () => '/orgs', diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 5656468e0..75c88f1af 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -3,7 +3,7 @@ import { validate as isUuid } from 'uuid' import * as mock from '@oxide/api-mocks' -import type { ApiTypes as Api, PathParams as PP, PathParamsV1 as PPv1 } from '@oxide/api' +import type { ApiTypes as Api, PathParams as PP } from '@oxide/api' import { user1 } from '@oxide/api-mocks' import { toApiSelector } from '@oxide/util' @@ -21,7 +21,7 @@ export const lookupById = (table: T[], id: string) => } export const lookup = { - org({ organization: id }: PPv1.Org): Json { + org({ organization: id }: PP.Org): Json { if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.orgs, id) @@ -31,7 +31,7 @@ export const lookup = { return org }, - project({ project: id, ...orgSelector }: PPv1.Project): Json { + project({ project: id, ...orgSelector }: PP.Project): Json { if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.projects, id) @@ -42,7 +42,7 @@ export const lookup = { return project }, - instance({ instance: id, ...projectSelector }: PPv1.Instance): Json { + instance({ instance: id, ...projectSelector }: PP.Instance): Json { if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.instances, id) @@ -56,7 +56,7 @@ export const lookup = { networkInterface({ interface: id, ...instanceSelector - }: PPv1.NetworkInterface): Json { + }: PP.NetworkInterface): Json { if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.networkInterfaces, id) @@ -70,7 +70,7 @@ export const lookup = { return nic }, - disk({ disk: id, ...projectSelector }: PPv1.Disk): Json { + disk({ disk: id, ...projectSelector }: PP.Disk): Json { if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.disks, id) @@ -82,7 +82,7 @@ export const lookup = { return disk }, - snapshot({ snapshot: id, ...projectSelector }: PPv1.Snapshot): Json { + snapshot({ snapshot: id, ...projectSelector }: PP.Snapshot): Json { if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.snapshots, id) @@ -93,7 +93,7 @@ export const lookup = { return snapshot }, - vpc({ vpc: id, ...projectSelector }: PPv1.Vpc): Json { + vpc({ vpc: id, ...projectSelector }: PP.Vpc): Json { console.log({ id, ...projectSelector }) if (!id) throw notFoundErr @@ -105,7 +105,7 @@ export const lookup = { return vpc }, - vpcRouter({ router: id, ...vpcSelector }: PPv1.VpcRouter): Json { + vpcRouter({ router: id, ...vpcSelector }: PP.VpcRouter): Json { if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.vpcRouters, id) @@ -116,10 +116,7 @@ export const lookup = { return router }, - vpcRouterRoute({ - route: id, - ...routerSelector - }: PPv1.RouterRoute): Json { + vpcRouterRoute({ route: id, ...routerSelector }: PP.RouterRoute): Json { if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.vpcRouterRoutes, id) @@ -132,7 +129,7 @@ export const lookup = { return route }, - vpcSubnet({ subnet: id, ...vpcSelector }: PPv1.VpcSubnet): Json { + vpcSubnet({ subnet: id, ...vpcSelector }: PP.VpcSubnet): Json { if (!id) throw notFoundErr if (isUuid(id)) return lookupById(db.vpcSubnets, id) @@ -143,12 +140,12 @@ export const lookup = { return subnet }, - systemUpdate({ version }: PPv1.SystemUpdate): Json { + systemUpdate({ version }: PP.SystemUpdate): Json { const update = db.systemUpdates.find((o) => o.version === version) if (!update) throw notFoundErr return update }, - sled(params: PPv1.Id): Json { + sled(params: PP.Id): Json { const sled = db.sleds.find((sled) => sled.id === params.id) if (!sled) throw notFoundErr return sled diff --git a/libs/api/index.ts b/libs/api/index.ts index 1c8295c8a..532950601 100644 --- a/libs/api/index.ts +++ b/libs/api/index.ts @@ -46,7 +46,6 @@ export * as ZVal from './__generated__/validate' export type { ApiTypes } export * as PathParams from './path-params' -export * as PathParamsV1 from './path-params-v1' export type { Params, Result, ResultItem } from './hooks' export { navToLogin } from './nav-to-login' diff --git a/libs/api/path-params-v1.ts b/libs/api/path-params-v1.ts deleted file mode 100644 index 9d5d5282f..000000000 --- a/libs/api/path-params-v1.ts +++ /dev/null @@ -1,18 +0,0 @@ -// these aren't really only path params anymore so we'll probably want to rename -// this file -import type { Merge } from 'type-fest' - -export type Org = { organization?: string } -export type Project = Merge -export type Instance = Merge -export type Disk = Merge -export type NetworkInterface = Merge -export type Snapshot = Merge -export type Vpc = Merge -export type VpcSubnet = Merge -export type VpcRouter = Merge -export type RouterRoute = Merge -export type SystemUpdate = { version: string } -export type Silo = { silo: string } - -export type Id = { id: string } diff --git a/libs/api/path-params.ts b/libs/api/path-params.ts index fe8650449..dc9d9d78a 100644 --- a/libs/api/path-params.ts +++ b/libs/api/path-params.ts @@ -1,22 +1,24 @@ import type { Merge } from 'type-fest' -export type Org = { orgName: string } -export type Project = Merge -export type Vpc = Merge -export type Instance = Merge -export type NetworkInterface = Merge -export type Disk = Merge -export type Image = Merge -export type Snapshot = Merge -export type DiskMetric = Merge -export type VpcSubnet = Merge -export type VpcRouter = Merge -export type VpcRouterRoute = Merge -export type SshKey = { sshKeyName: string } +export type Org = { organization?: string } +export type Project = Merge +export type Instance = Merge +export type Disk = Merge +export type NetworkInterface = Merge +export type Snapshot = Merge +export type Vpc = Merge +export type VpcSubnet = Merge +export type VpcRouter = Merge +export type RouterRoute = Merge +export type SystemUpdate = { version: string } +export type SiloV1 = { silo: string } + +export type Id = { id: string } + +// Not yet converted to v1 + +export type Image = { orgName: string; projectName: string; imageName: string } export type GlobalImage = { imageName: string } export type Silo = { siloName: string } export type IdentityProvider = Merge -export type Id = { id: string } -export type SystemMetric = { resourceName: string } -export type PhysicalDisk = { sledId: string } -export type SystemUpdate = { version: string } +export type SshKey = { sshKeyName: string } From ee77cbbd0591a5dede8ea98aa7a0286865eaab19 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 21 Feb 2023 14:58:23 -0600 Subject: [PATCH 39/39] don't be silly --- app/forms/project-create.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index bbfabd93a..4d6c7684f 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom' import type { ProjectCreate } from '@oxide/api' import { useApiMutation, useApiQueryClient } from '@oxide/api' import { Success16Icon } from '@oxide/ui' -import { toPathQuery } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { pb } from 'app/util/path-builder' @@ -29,10 +28,9 @@ export function CreateProjectSideModalForm() { // refetch list of projects in sidebar queryClient.invalidateQueries('projectListV1', { query: { organization } }) // avoid the project fetch when the project page loads since we have the data - const projectSelector = { organization, project: project.name } queryClient.setQueryData( 'projectViewV1', - toPathQuery('project', projectSelector), + { path: { project: project.name }, query: { organization } }, project ) addToast({ @@ -40,7 +38,7 @@ export function CreateProjectSideModalForm() { title: 'Success!', content: 'Your project has been created.', }) - navigate(pb.instances(projectSelector)) + navigate(pb.instances({ organization, project: project.name })) }, })