From 0e8f46860f7abe9028c34db50edcced50a869956 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 5 Feb 2024 13:39:34 -0800 Subject: [PATCH 01/64] Add Floating IP mock API calls --- libs/api-mocks/floating-ip.ts | 36 +++++++++++++++++++++++++++++++++++ libs/api-mocks/index.ts | 1 + 2 files changed, 37 insertions(+) create mode 100644 libs/api-mocks/floating-ip.ts diff --git a/libs/api-mocks/floating-ip.ts b/libs/api-mocks/floating-ip.ts new file mode 100644 index 0000000000..047d56488c --- /dev/null +++ b/libs/api-mocks/floating-ip.ts @@ -0,0 +1,36 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import type { FloatingIp } from '@oxide/api' + +import { instance, project } from '.' +import type { Json } from './json-type' + +// A floating IP from the default pool +export const floatingIp: Json = { + id: '3ca0ccb7-d66d-4fde-a871-ab9855eaea8e', + name: 'rootbeer-float', + description: 'A classic.', + instance_id: undefined, + ip: '192.168.32.1', + project_id: project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), +} + +// A floating IP attached to a particular instance +export const floatingIp2: Json = { + id: '0a00a6c3-4821-4bb8-af77-574468ac6651', + name: 'cola-float', + description: 'A favourite.', + instance_id: instance.id, + ip: '192.168.64.64', + project_id: project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), +} diff --git a/libs/api-mocks/index.ts b/libs/api-mocks/index.ts index 3cbc07b773..129856fe9c 100644 --- a/libs/api-mocks/index.ts +++ b/libs/api-mocks/index.ts @@ -8,6 +8,7 @@ export * from './disk' export * from './external-ip' +export * from './floating-ip' export * from './image' export * from './instance' export * from './ip-pool' From 458187deca13c8ee859bb08e61ef07f54f58e63b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 5 Feb 2024 15:07:00 -0800 Subject: [PATCH 02/64] Working on networking / routes for pages and tabs --- .../FloatingIpPage/FloatingIpPage.tsx | 68 +++++++++++ .../networking/FloatingIpPage/index.tsx | 9 ++ .../{VpcsPage.tsx => FloatingIpsTab.tsx} | 16 +-- .../networking/ProjectNetworkingPage.tsx | 27 +++++ app/pages/project/networking/VpcsTab.tsx | 107 ++++++++++++++++++ app/pages/project/networking/index.tsx | 2 +- app/routes.tsx | 73 ++++++++---- app/util/path-builder.ts | 6 +- 8 files changed, 271 insertions(+), 37 deletions(-) create mode 100644 app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx create mode 100644 app/pages/project/networking/FloatingIpPage/index.tsx rename app/pages/project/networking/{VpcsPage.tsx => FloatingIpsTab.tsx} (90%) create mode 100644 app/pages/project/networking/ProjectNetworkingPage.tsx create mode 100644 app/pages/project/networking/VpcsTab.tsx diff --git a/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx b/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx new file mode 100644 index 0000000000..dee18d76ce --- /dev/null +++ b/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx @@ -0,0 +1,68 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import type { LoaderFunctionArgs } from 'react-router-dom' + +import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' +import { Networking24Icon, PageHeader, PageTitle, PropertiesTable, Tabs } from '@oxide/ui' +import { formatDateTime } from '@oxide/util' + +import { QueryParamTabs } from 'app/components/QueryParamTabs' +import { getVpcSelector, useVpcSelector } from 'app/hooks' + +FloatingIpPage.loader = async ({ params }: LoaderFunctionArgs) => { + const { project, vpc } = getVpcSelector(params) + await Promise.all([ + apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } }), + apiQueryClient.prefetchQuery('vpcFirewallRulesView', { + query: { project, vpc }, + }), + apiQueryClient.prefetchQuery('vpcSubnetList', { + query: { project, vpc, limit: 25 }, + }), + ]) + return null +} + +export function FloatingIpPage() { + const { project, vpc: vpcName } = useVpcSelector() + const { data: vpc } = usePrefetchedApiQuery('vpcView', { + path: { vpc: vpcName }, + query: { project }, + }) + + return ( + <> + + }>{vpc.name} + + + + {vpc.description} + {vpc.dnsName} + + + + {formatDateTime(vpc.timeCreated)} + + + {formatDateTime(vpc.timeModified)} + + + + + + + Subnets + Firewall Rules + + x + y + + + ) +} diff --git a/app/pages/project/networking/FloatingIpPage/index.tsx b/app/pages/project/networking/FloatingIpPage/index.tsx new file mode 100644 index 0000000000..f4610d910a --- /dev/null +++ b/app/pages/project/networking/FloatingIpPage/index.tsx @@ -0,0 +1,9 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +export * from './FloatingIpPage' diff --git a/app/pages/project/networking/VpcsPage.tsx b/app/pages/project/networking/FloatingIpsTab.tsx similarity index 90% rename from app/pages/project/networking/VpcsPage.tsx rename to app/pages/project/networking/FloatingIpsTab.tsx index 286faa08f3..679072a080 100644 --- a/app/pages/project/networking/VpcsPage.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -16,14 +16,7 @@ import { type Vpc, } from '@oxide/api' import { DateCell, linkCell, useQueryTable, type MenuAction } from '@oxide/table' -import { - buttonStyle, - EmptyMessage, - Networking24Icon, - PageHeader, - PageTitle, - TableActions, -} from '@oxide/ui' +import { buttonStyle, EmptyMessage, Networking24Icon, TableActions } from '@oxide/ui' import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' import { confirmDelete } from 'app/stores/confirm-delete' @@ -41,14 +34,14 @@ const EmptyState = () => ( // just as in the vpcList call for the quick actions menu, include limit: 25 to make // sure it matches the call in the QueryTable -VpcsPage.loader = async ({ params }: LoaderFunctionArgs) => { +FloatingIpsTab.loader = async ({ params }: LoaderFunctionArgs) => { await apiQueryClient.prefetchQuery('vpcList', { query: { ...getProjectSelector(params), limit: 25 }, }) return null } -export function VpcsPage() { +export function FloatingIpsTab() { const queryClient = useApiQueryClient() const projectSelector = useProjectSelector() const { data: vpcs } = usePrefetchedApiQuery('vpcList', { @@ -94,9 +87,6 @@ export function VpcsPage() { const { Table, Column } = useQueryTable('vpcList', { query: projectSelector }) return ( <> - - }>VPCs - New Vpc diff --git a/app/pages/project/networking/ProjectNetworkingPage.tsx b/app/pages/project/networking/ProjectNetworkingPage.tsx new file mode 100644 index 0000000000..5fa9cbec9e --- /dev/null +++ b/app/pages/project/networking/ProjectNetworkingPage.tsx @@ -0,0 +1,27 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { Networking24Icon, PageHeader, PageTitle } from '@oxide/ui' + +import { RouteTabs, Tab } from 'app/components/RouteTabs' +import { useProjectSelector } from 'app/hooks' +import { pb } from 'app/util/path-builder' + +export function ProjectNetworkingPage() { + const projectSelector = useProjectSelector() + return ( + <> + + }>Networking + + + VPCs + Floating IPs + + + ) +} diff --git a/app/pages/project/networking/VpcsTab.tsx b/app/pages/project/networking/VpcsTab.tsx new file mode 100644 index 0000000000..053e759869 --- /dev/null +++ b/app/pages/project/networking/VpcsTab.tsx @@ -0,0 +1,107 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { useMemo } from 'react' +import { Link, Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' + +import { + apiQueryClient, + useApiMutation, + useApiQueryClient, + usePrefetchedApiQuery, + type Vpc, +} from '@oxide/api' +import { DateCell, linkCell, useQueryTable, type MenuAction } from '@oxide/table' +import { buttonStyle, EmptyMessage, Networking24Icon, TableActions } from '@oxide/ui' + +import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' +import { confirmDelete } from 'app/stores/confirm-delete' +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={pb.vpcNew(useProjectSelector())} + /> +) + +// just as in the vpcList call for the quick actions menu, include limit: 25 to make +// sure it matches the call in the QueryTable +VpcsTab.loader = async ({ params }: LoaderFunctionArgs) => { + await apiQueryClient.prefetchQuery('vpcList', { + query: { ...getProjectSelector(params), limit: 25 }, + }) + return null +} + +export function VpcsTab() { + const queryClient = useApiQueryClient() + const projectSelector = useProjectSelector() + const { data: vpcs } = usePrefetchedApiQuery('vpcList', { + query: { ...projectSelector, limit: 25 }, // to have same params as QueryTable + }) + const navigate = useNavigate() + + const deleteVpc = useApiMutation('vpcDelete', { + onSuccess() { + queryClient.invalidateQueries('vpcList') + }, + }) + + const makeActions = (vpc: Vpc): MenuAction[] => [ + { + label: 'Edit', + onActivate() { + navigate(pb.vpcEdit({ ...projectSelector, vpc: vpc.name }), { state: vpc }) + }, + }, + { + label: 'Delete', + onActivate: confirmDelete({ + doDelete: () => + deleteVpc.mutateAsync({ path: { vpc: vpc.name }, query: projectSelector }), + label: vpc.name, + }), + }, + ] + + useQuickActions( + useMemo( + () => + vpcs.items.map((v) => ({ + value: v.name, + onSelect: () => navigate(pb.vpc({ ...projectSelector, vpc: v.name })), + navGroup: 'Go to VPC', + })), + [projectSelector, vpcs, navigate] + ) + ) + + const { Table, Column } = useQueryTable('vpcList', { query: projectSelector }) + return ( + <> + + + New Vpc + + + } makeActions={makeActions}> + pb.vpc({ ...projectSelector, vpc }))} + /> + + + +
+ + + ) +} diff --git a/app/pages/project/networking/index.tsx b/app/pages/project/networking/index.tsx index 63acdb001a..8c607b502b 100644 --- a/app/pages/project/networking/index.tsx +++ b/app/pages/project/networking/index.tsx @@ -7,4 +7,4 @@ */ export * from './VpcPage' -export * from './VpcsPage' +export * from './VpcsTab' diff --git a/app/routes.tsx b/app/routes.tsx index 55f1095323..d6e925b75c 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -51,13 +51,16 @@ import { ProjectAccessPage, SnapshotsPage, VpcPage, - VpcsPage, + VpcsTab, } from './pages/project' import { SerialConsolePage } from './pages/project/instances/instance/SerialConsolePage' import { ConnectTab } from './pages/project/instances/instance/tabs/ConnectTab' import { MetricsTab } from './pages/project/instances/instance/tabs/MetricsTab' import { NetworkingTab } from './pages/project/instances/instance/tabs/NetworkingTab' import { StorageTab } from './pages/project/instances/instance/tabs/StorageTab' +import { FloatingIpPage } from './pages/project/networking/FloatingIpPage' +import { FloatingIpsTab } from './pages/project/networking/FloatingIpsTab' +import { ProjectNetworkingPage } from './pages/project/networking/ProjectNetworkingPage' import ProjectsPage from './pages/ProjectsPage' import { ProfilePage } from './pages/settings/ProfilePage' import { SSHKeysPage } from './pages/settings/SSHKeysPage' @@ -79,6 +82,7 @@ import { pb } from './util/path-builder' const projectCrumb: CrumbFunc = (m) => m.params.project! const instanceCrumb: CrumbFunc = (m) => m.params.instance! +const floatingIpcCrumb: CrumbFunc = (m) => m.params.floatingIp! const vpcCrumb: CrumbFunc = (m) => m.params.vpc! const siloCrumb: CrumbFunc = (m) => m.params.silo! @@ -316,28 +320,55 @@ export const routes = createRoutesFromElements( - }> - - } - handle={{ crumb: 'New VPC' }} - /> - } - loader={EditVpcSideModalForm.loader} - handle={{ crumb: 'Edit VPC' }} - /> + }> + }> + + } + handle={{ crumb: 'New VPC' }} + /> + } + loader={EditVpcSideModalForm.loader} + handle={{ crumb: 'Edit VPC' }} + /> + + + }> + + {/* } + handle={{ crumb: 'New Floating IP' }} + /> */} + {/* } + loader={EditFloatingIpSideModalForm.loader} + handle={{ crumb: 'Edit Floating IP' }} + /> */} + - - } - loader={VpcPage.loader} - handle={{ crumb: vpcCrumb }} - /> + + + } + loader={VpcPage.loader} + handle={{ crumb: vpcCrumb }} + /> + + + } + loader={FloatingIpPage.loader} + handle={{ crumb: floatingIpcCrumb }} + /> + } loader={DisksPage.loader}> diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index d4a5af14c5..fe79a7e301 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -62,10 +62,12 @@ export const pb = { snapshotImageCreate: (params: Snapshot) => `${pb.project(params)}/snapshots/${params.snapshot}/image-new`, - vpcNew: (params: Project) => `${pb.project(params)}/vpcs-new`, - vpcs: (params: Project) => `${pb.project(params)}/vpcs`, + projectNetworking: (params: Project) => `${pb.project(params)}/networking`, + vpcNew: (params: Project) => `${pb.projectNetworking(params)}/vpcs-new`, + vpcs: (params: Project) => `${pb.projectNetworking(params)}/vpcs`, vpc: (params: Vpc) => `${pb.vpcs(params)}/${params.vpc}`, vpcEdit: (params: Vpc) => `${pb.vpc(params)}/edit`, + floatingIps: (params: Project) => `${pb.projectNetworking(params)}/floating-ips`, siloUtilization: () => '/utilization', siloAccess: () => '/access', From 6bd6c1422e4d31325788602ce6c00dc6b9e26f72 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 5 Feb 2024 17:02:27 -0800 Subject: [PATCH 03/64] Adjusting from VPC base to Floating IP implementation --- .../project/networking/FloatingIpsTab.tsx | 55 +++++++++++-------- app/util/path-builder.ts | 4 ++ libs/api/path-params.ts | 1 + 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 679072a080..f860ea2bec 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -13,7 +13,7 @@ import { useApiMutation, useApiQueryClient, usePrefetchedApiQuery, - type Vpc, + type FloatingIp, } from '@oxide/api' import { DateCell, linkCell, useQueryTable, type MenuAction } from '@oxide/table' import { buttonStyle, EmptyMessage, Networking24Icon, TableActions } from '@oxide/ui' @@ -25,17 +25,16 @@ 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={pb.vpcNew(useProjectSelector())} + title="No Floating IPs" + body="You need to create a Floating IP to be able to see it here" + buttonText="New Floating IP" + buttonTo={pb.floatingIpNew(useProjectSelector())} /> ) -// just as in the vpcList call for the quick actions menu, include limit: 25 to make -// sure it matches the call in the QueryTable +// 🐛👀 This isn't actually prefetching the Floating IPs at the moment FloatingIpsTab.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('vpcList', { + await apiQueryClient.prefetchQuery('floatingIpList', { query: { ...getProjectSelector(params), limit: 25 }, }) return null @@ -44,30 +43,35 @@ FloatingIpsTab.loader = async ({ params }: LoaderFunctionArgs) => { export function FloatingIpsTab() { const queryClient = useApiQueryClient() const projectSelector = useProjectSelector() - const { data: vpcs } = usePrefetchedApiQuery('vpcList', { + const { data: floatingIps } = usePrefetchedApiQuery('floatingIpList', { query: { ...projectSelector, limit: 25 }, // to have same params as QueryTable }) const navigate = useNavigate() - const deleteVpc = useApiMutation('vpcDelete', { + const deleteFloatingIp = useApiMutation('floatingIpDelete', { onSuccess() { - queryClient.invalidateQueries('vpcList') + queryClient.invalidateQueries('floatingIpList') }, }) - const makeActions = (vpc: Vpc): MenuAction[] => [ + const makeActions = (floatingIp: FloatingIp): MenuAction[] => [ { label: 'Edit', onActivate() { - navigate(pb.vpcEdit({ ...projectSelector, vpc: vpc.name }), { state: vpc }) + navigate(pb.floatingIpEdit({ ...projectSelector, floatingIp: floatingIp.name }), { + state: floatingIp, + }) }, }, { label: 'Delete', onActivate: confirmDelete({ doDelete: () => - deleteVpc.mutateAsync({ path: { vpc: vpc.name }, query: projectSelector }), - label: vpc.name, + deleteFloatingIp.mutateAsync({ + path: { floatingIp: floatingIp.name }, + query: projectSelector, + }), + label: floatingIp.name, }), }, ] @@ -75,29 +79,32 @@ export function FloatingIpsTab() { useQuickActions( useMemo( () => - vpcs.items.map((v) => ({ + floatingIps.items.map((v) => ({ value: v.name, - onSelect: () => navigate(pb.vpc({ ...projectSelector, vpc: v.name })), - navGroup: 'Go to VPC', + onSelect: () => + navigate(pb.floatingIp({ ...projectSelector, floatingIp: v.name })), + navGroup: 'Go to Floating IP', })), - [projectSelector, vpcs, navigate] + [projectSelector, floatingIps, navigate] ) ) - const { Table, Column } = useQueryTable('vpcList', { query: projectSelector }) + const { Table, Column } = useQueryTable('floatingIpList', { query: projectSelector }) return ( <> - - New Vpc + + New FloatingIp } makeActions={makeActions}> pb.vpc({ ...projectSelector, vpc }))} + cell={linkCell((floatingIp) => pb.floatingIp({ ...projectSelector, floatingIp }))} /> -
diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index fe79a7e301..6aa1a75087 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -20,6 +20,7 @@ type Image = Required type Snapshot = Required type SiloImage = Required type IpPool = Required +type FloatingIp = Required export const pb = { projects: () => `/projects`, @@ -68,6 +69,9 @@ export const pb = { vpc: (params: Vpc) => `${pb.vpcs(params)}/${params.vpc}`, vpcEdit: (params: Vpc) => `${pb.vpc(params)}/edit`, floatingIps: (params: Project) => `${pb.projectNetworking(params)}/floating-ips`, + floatingIpNew: (params: Project) => `${pb.projectNetworking(params)}/floating-ips-new`, + floatingIp: (params: FloatingIp) => `${pb.floatingIps(params)}/${params.floatingIp}`, + floatingIpEdit: (params: FloatingIp) => `${pb.floatingIp(params)}/edit`, siloUtilization: () => '/utilization', siloAccess: () => '/access', diff --git a/libs/api/path-params.ts b/libs/api/path-params.ts index 8f2ccc8270..dc5c30a9af 100644 --- a/libs/api/path-params.ts +++ b/libs/api/path-params.ts @@ -22,5 +22,6 @@ export type SystemUpdate = { version: string } export type SshKey = { sshKey: string } export type Sled = { sledId?: string } export type IpPool = { pool?: string } +export type FloatingIp = Merge export type Id = { id: string } From 187e04bf4dec38221270c5053b01d5f254ce534e Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 6 Feb 2024 13:40:46 -0800 Subject: [PATCH 04/64] Adjust vertical spacing on tabbed pages with table action buttons --- app/pages/project/networking/FloatingIpsTab.tsx | 6 +++--- app/pages/project/networking/VpcsTab.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index f860ea2bec..154dff1d5d 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -16,7 +16,7 @@ import { type FloatingIp, } from '@oxide/api' import { DateCell, linkCell, useQueryTable, type MenuAction } from '@oxide/table' -import { buttonStyle, EmptyMessage, Networking24Icon, TableActions } from '@oxide/ui' +import { buttonStyle, EmptyMessage, Networking24Icon } from '@oxide/ui' import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' import { confirmDelete } from 'app/stores/confirm-delete' @@ -92,14 +92,14 @@ export function FloatingIpsTab() { const { Table, Column } = useQueryTable('floatingIpList', { query: projectSelector }) return ( <> - +
New FloatingIp - +
} makeActions={makeActions}> - +
New Vpc - +
} makeActions={makeActions}> Date: Tue, 6 Feb 2024 13:52:20 -0800 Subject: [PATCH 05/64] Add Floating IP to mock API calls --- app/pages/project/networking/FloatingIpsTab.tsx | 2 +- libs/api-mocks/floating-ip.ts | 2 ++ libs/api-mocks/msw/db.ts | 14 ++++++++++++++ libs/api-mocks/msw/handlers.ts | 6 +++++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 154dff1d5d..349ffe2b1f 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -97,7 +97,7 @@ export function FloatingIpsTab() { to={pb.floatingIpNew(projectSelector)} className={buttonStyle({ size: 'sm' })} > - New FloatingIp + New Floating IP
} makeActions={makeActions}> diff --git a/libs/api-mocks/floating-ip.ts b/libs/api-mocks/floating-ip.ts index 047d56488c..08c3ca5ff8 100644 --- a/libs/api-mocks/floating-ip.ts +++ b/libs/api-mocks/floating-ip.ts @@ -34,3 +34,5 @@ export const floatingIp2: Json = { time_created: new Date().toISOString(), time_modified: new Date().toISOString(), } + +export const floatingIps = [floatingIp, floatingIp2] diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index a5d9d27c67..5c29f77aa2 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -81,6 +81,19 @@ export const lookup = { return disk }, + floatingIp({ floatingIp: id, ...projectSelector }: PP.FloatingIp): Json { + if (!id) throw notFoundErr + + if (isUuid(id)) return lookupById(db.floatingIps, id) + + const project = lookup.project(projectSelector) + const floatingIp = db.floatingIps.find( + (i) => i.project_id === project.id && i.name === id + ) + if (!floatingIp) throw notFoundErr + + return floatingIp + }, snapshot({ snapshot: id, ...projectSelector }: PP.Snapshot): Json { if (!id) throw notFoundErr @@ -245,6 +258,7 @@ type DiskBulkImport = { const initDb = { disks: [...mock.disks], diskBulkImportState: new Map(), + floatingIps: [...mock.floatingIps], userGroups: [...mock.userGroups], /** Join table for `users` and `userGroups` */ groupMemberships: [...mock.groupMemberships], diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 4e0efd6221..b57ec1c83e 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -223,6 +223,11 @@ export const handlers = makeHandlers({ return 204 }, + floatingIpList({ query }) { + const project = lookup.project(query) + const ips = db.floatingIps.filter((i) => i.project_id === project.id) + return paginated(query, ips) + }, imageList({ query }) { if (query.project) { const project = lookup.project(query) @@ -1127,7 +1132,6 @@ export const handlers = makeHandlers({ certificateView: NotImplemented, floatingIpCreate: NotImplemented, floatingIpDelete: NotImplemented, - floatingIpList: NotImplemented, floatingIpView: NotImplemented, floatingIpAttach: NotImplemented, floatingIpDetach: NotImplemented, From 672266488d89a725078da0a37a18285f40d0abe8 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 6 Feb 2024 15:37:49 -0800 Subject: [PATCH 06/64] Update columns --- app/pages/project/networking/FloatingIpsTab.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 349ffe2b1f..181abdb39c 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -15,7 +15,7 @@ import { usePrefetchedApiQuery, type FloatingIp, } from '@oxide/api' -import { DateCell, linkCell, useQueryTable, type MenuAction } from '@oxide/table' +import { linkCell, useQueryTable, type MenuAction } from '@oxide/table' import { buttonStyle, EmptyMessage, Networking24Icon } from '@oxide/ui' import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' @@ -32,7 +32,6 @@ const EmptyState = () => ( /> ) -// 🐛👀 This isn't actually prefetching the Floating IPs at the moment FloatingIpsTab.loader = async ({ params }: LoaderFunctionArgs) => { await apiQueryClient.prefetchQuery('floatingIpList', { query: { ...getProjectSelector(params), limit: 25 }, @@ -46,6 +45,7 @@ export function FloatingIpsTab() { const { data: floatingIps } = usePrefetchedApiQuery('floatingIpList', { query: { ...projectSelector, limit: 25 }, // to have same params as QueryTable }) + const navigate = useNavigate() const deleteFloatingIp = useApiMutation('floatingIpDelete', { @@ -106,7 +106,8 @@ export function FloatingIpsTab() { cell={linkCell((floatingIp) => pb.floatingIp({ ...projectSelector, floatingIp }))} /> - + +
From a5ea03a48a2f9a0bea20106e5579871b6f58b8da Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 6 Feb 2024 16:49:50 -0800 Subject: [PATCH 07/64] Add form for Floating IP create --- app/forms/floating-ip-create.tsx | 101 +++++++++++++++++++++++++++++++ app/routes.tsx | 5 +- libs/api-mocks/msw/handlers.ts | 16 ++++- 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 app/forms/floating-ip-create.tsx diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx new file mode 100644 index 0000000000..b3a395976c --- /dev/null +++ b/app/forms/floating-ip-create.tsx @@ -0,0 +1,101 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { useNavigate, type NavigateFunction } from 'react-router-dom' + +import { + useApiMutation, + useApiQueryClient, + type FloatingIp, + type FloatingIpCreate, +} from '@oxide/api' + +import { DescriptionField, NameField, SideModalForm } from 'app/components/form' +import { useForm, useProjectSelector, useToast } from 'app/hooks' +import { pb } from 'app/util/path-builder' + +const defaultValues: FloatingIpCreate = { + address: '', + description: '', + name: '', + pool: '', +} + +type CreateSideModalFormProps = { + /** + * If defined, this overrides the usual mutation. Caller is responsible for + * doing a dismiss behavior in onSubmit as well, because we are not calling + * the RQ `onSuccess` defined for the mutation. + */ + onSubmit?: (floatingIpCreate: FloatingIpCreate) => void + /** + * Passing navigate is a bit of a hack to be able to do a nav from the routes + * file. The callers that don't need the arg can ignore it. + */ + onDismiss?: (navigate: NavigateFunction) => void + onSuccess?: (floatingIp: FloatingIp) => void +} + +export function CreateFloatingIpSideModalForm({ + onSubmit, + onSuccess, +}: CreateSideModalFormProps) { + const { project } = useProjectSelector() + console.log(project) + + // Fetch 1000 to we can be sure to get them all. There should only be a few + // anyway. Not prefetched because the prefetched one only gets 25 to match the + // query table. This req is better to do async because they can't click make + // default that fast anyway. + // const { data: allPools } = useApiQuery('siloIpPoolList', { + // path: { project }, + // query: { limit: 1000 }, + // }) + // console.log(allPools) + + // used in change default confirm modal + // const defaultPool = useMemo( + // () => (allPools ? allPools.items.find((p) => p.isDefault)?.name : undefined), + // [allPools] + // ) + + const queryClient = useApiQueryClient() + const projectSelector = useProjectSelector() + const addToast = useToast() + const navigate = useNavigate() + + const createFloatingIp = useApiMutation('floatingIpCreate', { + onSuccess(data) { + queryClient.invalidateQueries('floatingIpList') + addToast({ content: 'Your Floating IP has been created' }) + onSuccess?.(data) + navigate(pb.floatingIps(projectSelector)) + }, + }) + + const form = useForm({ defaultValues }) + + return ( + navigate(pb.floatingIps(projectSelector))} + onSubmit={({ ...rest }) => { + const body = { ...rest } + onSubmit + ? onSubmit(body) + : createFloatingIp.mutate({ query: projectSelector, body }) + }} + loading={createFloatingIp.isPending} + submitError={createFloatingIp.error} + > + + + + ) +} diff --git a/app/routes.tsx b/app/routes.tsx index d6e925b75c..c132cc97b8 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -10,6 +10,7 @@ import { createRoutesFromElements, Navigate, Route } from 'react-router-dom' import { RouterDataErrorBoundary } from './components/ErrorBoundary' import { NotFound } from './components/ErrorPage' import { CreateDiskSideModalForm } from './forms/disk-create' +import { CreateFloatingIpSideModalForm } from './forms/floating-ip-create' import { CreateIdpSideModalForm } from './forms/idp/create' import { EditIdpSideModalForm } from './forms/idp/edit' import { @@ -338,11 +339,11 @@ export const routes = createRoutesFromElements( }> - {/* } handle={{ crumb: 'New Floating IP' }} - /> */} + /> {/* } diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index b57ec1c83e..ca316865ec 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -223,6 +223,21 @@ export const handlers = makeHandlers({ return 204 }, + floatingIpCreate({ body, query }) { + const project = lookup.project(query) + errIfExists(db.vpcs, { name: body.name }) + + const newFloatingIp: Json = { + id: uuid(), + project_id: project.id, + ip: '', // 👀 needs a legit ip + ...body, + ...getTimestamps(), + } + db.floatingIps.push(newFloatingIp) + return json(newFloatingIp, { status: 201 }) + }, + floatingIpList({ query }) { const project = lookup.project(query) const ips = db.floatingIps.filter((i) => i.project_id === project.id) @@ -1130,7 +1145,6 @@ export const handlers = makeHandlers({ certificateDelete: NotImplemented, certificateList: NotImplemented, certificateView: NotImplemented, - floatingIpCreate: NotImplemented, floatingIpDelete: NotImplemented, floatingIpView: NotImplemented, floatingIpAttach: NotImplemented, From bdad83369b039fd67bc5290dc3eb38d290f0710b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 7 Feb 2024 12:47:07 -0800 Subject: [PATCH 08/64] Update actions list --- app/forms/floating-ip-create.tsx | 2 + .../project/networking/FloatingIpsTab.tsx | 59 ++++++++++++------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index b3a395976c..4174a52406 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -96,6 +96,8 @@ export function CreateFloatingIpSideModalForm({ > +

Pool Select

+

IP Address select

) } diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 181abdb39c..126ba311c1 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -54,27 +54,46 @@ export function FloatingIpsTab() { }, }) - const makeActions = (floatingIp: FloatingIp): MenuAction[] => [ - { - label: 'Edit', - onActivate() { - navigate(pb.floatingIpEdit({ ...projectSelector, floatingIp: floatingIp.name }), { - state: floatingIp, - }) + const makeActions = (floatingIp: FloatingIp): MenuAction[] => { + const isAttachedToAnInstance = floatingIp.instanceId !== null + return [ + { + label: 'Edit', + onActivate() { + navigate(pb.floatingIpEdit({ ...projectSelector, floatingIp: floatingIp.name }), { + state: floatingIp, + }) + }, }, - }, - { - label: 'Delete', - onActivate: confirmDelete({ - doDelete: () => - deleteFloatingIp.mutateAsync({ - path: { floatingIp: floatingIp.name }, - query: projectSelector, - }), - label: floatingIp.name, - }), - }, - ] + { + label: 'Attach', + // this should be available even if the floating IP is already attached + onActivate() { + // Open a modal to attach the floating IP to an instance + }, + }, + { + label: 'Detach', + disabled: !isAttachedToAnInstance, + onActivate() { + // Open a modal to attach the floating IP to an instance + }, + }, + { + label: 'Delete', + // Only available if the floating IP is not attached + disabled: isAttachedToAnInstance, + onActivate: confirmDelete({ + doDelete: () => + deleteFloatingIp.mutateAsync({ + path: { floatingIp: floatingIp.name }, + query: projectSelector, + }), + label: floatingIp.name, + }), + }, + ] + } useQuickActions( useMemo( From 717b96746411e6cc88f54dec8918c5518d17a147 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 7 Feb 2024 15:36:24 -0800 Subject: [PATCH 09/64] Get routing on floatingIpView working, though will remove it soon --- app/hooks/use-params.ts | 2 + app/main.tsx | 4 +- .../FloatingIpPage/FloatingIpPage.tsx | 57 ++++++++++--------- app/routes.tsx | 4 +- libs/api-mocks/msw/handlers.ts | 8 ++- 5 files changed, 42 insertions(+), 33 deletions(-) diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index 6a1293d8f1..a53e4ea30b 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -33,6 +33,7 @@ export const requireParams = } export const getProjectSelector = requireParams('project') +export const getFloatingIpSelector = requireParams('project', 'floatingIp') export const getInstanceSelector = requireParams('project', 'instance') export const getVpcSelector = requireParams('project', 'vpc') export const getSiloSelector = requireParams('silo') @@ -69,6 +70,7 @@ function useSelectedParams(getSelector: (params: AllParams) => T) { // params are present. Only the specified keys end up in the result object, but // we do not error if there are other params present in the query string. +export const useFloatingIpSelector = () => useSelectedParams(getFloatingIpSelector) export const useProjectSelector = () => useSelectedParams(getProjectSelector) export const useProjectImageSelector = () => useSelectedParams(getProjectImageSelector) export const useProjectSnapshotSelector = () => diff --git a/app/main.tsx b/app/main.tsx index 4cc71bf5c5..8c7e77e40d 100644 --- a/app/main.tsx +++ b/app/main.tsx @@ -6,7 +6,7 @@ * Copyright Oxide Computer Company */ import { QueryClientProvider } from '@tanstack/react-query' -// import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { createBrowserRouter, RouterProvider } from 'react-router-dom' @@ -50,7 +50,7 @@ function render() { - {/* */} + ) diff --git a/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx b/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx index dee18d76ce..177c5f6ff6 100644 --- a/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx +++ b/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx @@ -8,61 +8,62 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' -import { Networking24Icon, PageHeader, PageTitle, PropertiesTable, Tabs } from '@oxide/ui' +import { Networking24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui' import { formatDateTime } from '@oxide/util' -import { QueryParamTabs } from 'app/components/QueryParamTabs' -import { getVpcSelector, useVpcSelector } from 'app/hooks' +import { getFloatingIpSelector, useFloatingIpSelector } from 'app/hooks' FloatingIpPage.loader = async ({ params }: LoaderFunctionArgs) => { - const { project, vpc } = getVpcSelector(params) + const { project, floatingIp } = getFloatingIpSelector(params) await Promise.all([ - apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } }), - apiQueryClient.prefetchQuery('vpcFirewallRulesView', { - query: { project, vpc }, - }), - apiQueryClient.prefetchQuery('vpcSubnetList', { - query: { project, vpc, limit: 25 }, + // fetch all instances so we can map their id to their name + apiQueryClient.prefetchQuery('instanceList', { query: { project } }), + + // const { data } = usePrefetchedApiQuery('imageView', { path: { image } }) + + apiQueryClient.prefetchQuery('floatingIpView', { + path: { floatingIp }, + query: { project }, }), ]) return null } export function FloatingIpPage() { - const { project, vpc: vpcName } = useVpcSelector() - const { data: vpc } = usePrefetchedApiQuery('vpcView', { - path: { vpc: vpcName }, + // get the project name from the url + const { project, floatingIp } = useFloatingIpSelector() + // set the instances to the data from the instanceList query and console log them + const { data: instances } = usePrefetchedApiQuery('instanceList', { query: { project } }) + console.log(instances) + // get the floatingIp data + // const { data } = usePrefetchedApiQuery('imageView', { path: { image } }) + const { data: fip } = usePrefetchedApiQuery('floatingIpView', { + path: { floatingIp }, query: { project }, }) - + console.log(fip) return ( <> - }>{vpc.name} + }>{fip.name} - {vpc.description} - {vpc.dnsName} + {fip.description} - {formatDateTime(vpc.timeCreated)} + {formatDateTime(fip.timeCreated)} - {formatDateTime(vpc.timeModified)} + {formatDateTime(fip.timeModified)} - - - - Subnets - Firewall Rules - - x - y - +

+ We, uh, don’t actually need a page for this; this was more of a proof-of-concept as + I figured out routing and data fetching +

) } diff --git a/app/routes.tsx b/app/routes.tsx index c132cc97b8..0780faaf0a 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -83,7 +83,7 @@ import { pb } from './util/path-builder' const projectCrumb: CrumbFunc = (m) => m.params.project! const instanceCrumb: CrumbFunc = (m) => m.params.instance! -const floatingIpcCrumb: CrumbFunc = (m) => m.params.floatingIp! +const floatingIpCrumb: CrumbFunc = (m) => m.params.floatingIp! const vpcCrumb: CrumbFunc = (m) => m.params.vpc! const siloCrumb: CrumbFunc = (m) => m.params.silo! @@ -367,7 +367,7 @@ export const routes = createRoutesFromElements( path=":floatingIp" element={} loader={FloatingIpPage.loader} - handle={{ crumb: floatingIpcCrumb }} + handle={{ crumb: floatingIpCrumb }} />
diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index ca316865ec..ec0b69c369 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -243,6 +243,13 @@ export const handlers = makeHandlers({ const ips = db.floatingIps.filter((i) => i.project_id === project.id) return paginated(query, ips) }, + floatingIpView({ query, path }) { + const project = lookup.project(query) + const ip = db.floatingIps.filter( + (i) => i.project_id === project.id && i.name === path.floatingIp + )[0] + return ip + }, imageList({ query }) { if (query.project) { const project = lookup.project(query) @@ -1146,7 +1153,6 @@ export const handlers = makeHandlers({ certificateList: NotImplemented, certificateView: NotImplemented, floatingIpDelete: NotImplemented, - floatingIpView: NotImplemented, floatingIpAttach: NotImplemented, floatingIpDetach: NotImplemented, instanceEphemeralIpDetach: NotImplemented, From 86b7fcb31cbf3789b5aef845fb4e72ca562b4f41 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 8 Feb 2024 11:28:10 -0800 Subject: [PATCH 10/64] Get attached instance name into Floating IPs table --- .../project/networking/FloatingIpsTab.tsx | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 126ba311c1..4296ef0154 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -33,19 +33,27 @@ const EmptyState = () => ( ) FloatingIpsTab.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('floatingIpList', { - query: { ...getProjectSelector(params), limit: 25 }, - }) + const { project } = getProjectSelector(params) + await Promise.all([ + apiQueryClient.prefetchQuery('floatingIpList', { + query: { project, limit: 25 }, + }), + apiQueryClient.prefetchQuery('instanceList', { + query: { project }, + }), + ]) return null } export function FloatingIpsTab() { const queryClient = useApiQueryClient() - const projectSelector = useProjectSelector() + const { project } = useProjectSelector() const { data: floatingIps } = usePrefetchedApiQuery('floatingIpList', { - query: { ...projectSelector, limit: 25 }, // to have same params as QueryTable + query: { project, limit: 25 }, // to have same params as QueryTable + }) + const { data: instances } = usePrefetchedApiQuery('instanceList', { + query: { project }, }) - const navigate = useNavigate() const deleteFloatingIp = useApiMutation('floatingIpDelete', { @@ -60,7 +68,7 @@ export function FloatingIpsTab() { { label: 'Edit', onActivate() { - navigate(pb.floatingIpEdit({ ...projectSelector, floatingIp: floatingIp.name }), { + navigate(pb.floatingIpEdit({ project, floatingIp: floatingIp.name }), { state: floatingIp, }) }, @@ -87,7 +95,7 @@ export function FloatingIpsTab() { doDelete: () => deleteFloatingIp.mutateAsync({ path: { floatingIp: floatingIp.name }, - query: projectSelector, + query: { project }, }), label: floatingIp.name, }), @@ -100,33 +108,35 @@ export function FloatingIpsTab() { () => floatingIps.items.map((v) => ({ value: v.name, - onSelect: () => - navigate(pb.floatingIp({ ...projectSelector, floatingIp: v.name })), + onSelect: () => navigate(pb.floatingIp({ project, floatingIp: v.name })), navGroup: 'Go to Floating IP', })), - [projectSelector, floatingIps, navigate] + [project, floatingIps, navigate] ) ) + const getInstanceName = (instanceId: string) => + instances.items.find((i) => i.id === instanceId)?.name - const { Table, Column } = useQueryTable('floatingIpList', { query: projectSelector }) + const { Table, Column } = useQueryTable('floatingIpList', { query: { project } }) return ( <>
- + New Floating IP
} makeActions={makeActions}> pb.floatingIp({ ...projectSelector, floatingIp }))} + cell={linkCell((floatingIp) => pb.floatingIp({ project, floatingIp }))} /> - + getInstanceName(instanceId)} + />
From 4a4d8135029ab14c914a64d23e5c1c0b49e3385b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 8 Feb 2024 14:45:30 -0800 Subject: [PATCH 11/64] Get pools loading in Floating IP create form --- app/forms/floating-ip-create.tsx | 73 ++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index 4174a52406..eae0ef3e5b 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -5,24 +5,37 @@ * * Copyright Oxide Computer Company */ -import { useNavigate, type NavigateFunction } from 'react-router-dom' +import { useMemo } from 'react' +import { + useNavigate, + type LoaderFunctionArgs, + type NavigateFunction, +} from 'react-router-dom' import { + apiQueryClient, useApiMutation, + useApiQuery, useApiQueryClient, type FloatingIp, type FloatingIpCreate, } from '@oxide/api' -import { DescriptionField, NameField, SideModalForm } from 'app/components/form' -import { useForm, useProjectSelector, useToast } from 'app/hooks' +import { + DescriptionField, + ListboxField, + NameField, + SideModalForm, + TextField, +} from 'app/components/form' +import { getProjectSelector, useForm, useProjectSelector, useToast } from 'app/hooks' import { pb } from 'app/util/path-builder' -const defaultValues: FloatingIpCreate = { - address: '', - description: '', - name: '', - pool: '', +CreateFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { + await apiQueryClient.prefetchQuery('projectIpPoolList', { + query: { ...getProjectSelector(params), limit: 1000 }, + }) + return null } type CreateSideModalFormProps = { @@ -44,24 +57,26 @@ export function CreateFloatingIpSideModalForm({ onSubmit, onSuccess, }: CreateSideModalFormProps) { - const { project } = useProjectSelector() - console.log(project) - // Fetch 1000 to we can be sure to get them all. There should only be a few // anyway. Not prefetched because the prefetched one only gets 25 to match the // query table. This req is better to do async because they can't click make // default that fast anyway. - // const { data: allPools } = useApiQuery('siloIpPoolList', { - // path: { project }, - // query: { limit: 1000 }, - // }) - // console.log(allPools) + const { data: allPools } = useApiQuery('projectIpPoolList', { + query: { limit: 1000 }, + }) + + const defaultPool = useMemo( + () => (allPools ? allPools.items.find((p) => p.isDefault)?.name : undefined), + [allPools] + ) - // used in change default confirm modal - // const defaultPool = useMemo( - // () => (allPools ? allPools.items.find((p) => p.isDefault)?.name : undefined), - // [allPools] - // ) + const defaultValues: FloatingIpCreate = { + name: '', + description: '', + // defaultPool doesn't seem to be getting set in the form for some reason + pool: defaultPool, + address: undefined, + } const queryClient = useApiQueryClient() const projectSelector = useProjectSelector() @@ -96,8 +111,20 @@ export function CreateFloatingIpSideModalForm({ > -

Pool Select

-

IP Address select

+ {allPools && ( + ({ value: p.name, label: p.name }))} + label="Pool" + required + control={form.control} + /> + )} + (ip.trim() === '' ? undefined : ip)} + /> ) } From 6b00cd21aec6f37e903b6a7ab2ed1c11dc1c58dc Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 8 Feb 2024 16:33:55 -0800 Subject: [PATCH 12/64] Add IP Address field validation --- app/forms/floating-ip-create.tsx | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index eae0ef3e5b..21048a068e 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -6,6 +6,7 @@ * Copyright Oxide Computer Company */ import { useMemo } from 'react' +import { useWatch } from 'react-hook-form' import { useNavigate, type LoaderFunctionArgs, @@ -19,6 +20,7 @@ import { useApiQueryClient, type FloatingIp, type FloatingIpCreate, + type SiloIpPool, } from '@oxide/api' import { @@ -73,7 +75,7 @@ export function CreateFloatingIpSideModalForm({ const defaultValues: FloatingIpCreate = { name: '', description: '', - // defaultPool doesn't seem to be getting set in the form for some reason + // defaultPool doesn't seem to be getting set in the form when page is loaded directly pool: defaultPool, address: undefined, } @@ -94,6 +96,27 @@ export function CreateFloatingIpSideModalForm({ const form = useForm({ defaultValues }) + const toListboxItem = (p: SiloIpPool) => ({ + value: p.name, + label: p.name === defaultPool ? `${p.name} (default)` : p.name, + }) + + const validateIpAddress = (ip?: string) => { + if (!ip) return + + // regex from https://stackoverflow.com/a/26671130 + if ( + !/^(?=\d+\.\d+\.\d+\.\d+$)(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.?){4}$/.test( + ip + ) + ) { + return 'Must match the format of an IPv4 address' + } + } + + const [poolName] = useWatch({ control: form.control, name: ['pool'] }) + const isPoolSelected = poolName && poolName.length > 0 + return ( ({ value: p.name, label: p.name }))} + items={allPools?.items.map((p) => toListboxItem(p))} label="Pool" required control={form.control} @@ -123,7 +146,9 @@ export function CreateFloatingIpSideModalForm({ (ip.trim() === '' ? undefined : ip)} + validate={(ip) => validateIpAddress(ip)} /> ) From 2342a613e11b8dfed900921bc61e7377c119f368 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 8 Feb 2024 18:04:42 -0800 Subject: [PATCH 13/64] Update formatting on dropdown --- app/forms/floating-ip-create.tsx | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index 21048a068e..18994639b7 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -96,10 +96,25 @@ export function CreateFloatingIpSideModalForm({ const form = useForm({ defaultValues }) - const toListboxItem = (p: SiloIpPool) => ({ - value: p.name, - label: p.name === defaultPool ? `${p.name} (default)` : p.name, - }) + const toListboxItem = (p: SiloIpPool) => { + if (p.name !== defaultPool) { + return { + value: p.name, + label: p.name, + } + } + // For the default pool, add a label to the dropdown + return { + value: p.name, + labelString: p.name, + label: ( + <> + {p.name}{' '} + (default) + + ), + } + } const validateIpAddress = (ip?: string) => { if (!ip) return From bb587e317af461aa605e2c35e338094f77234d3a Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 9 Feb 2024 13:52:14 -0800 Subject: [PATCH 14/64] Refactor validation --- app/forms/floating-ip-create.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index 18994639b7..b23145a63f 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -22,6 +22,7 @@ import { type FloatingIpCreate, type SiloIpPool, } from '@oxide/api' +import { validateIp } from '@oxide/util' import { DescriptionField, @@ -116,19 +117,6 @@ export function CreateFloatingIpSideModalForm({ } } - const validateIpAddress = (ip?: string) => { - if (!ip) return - - // regex from https://stackoverflow.com/a/26671130 - if ( - !/^(?=\d+\.\d+\.\d+\.\d+$)(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.?){4}$/.test( - ip - ) - ) { - return 'Must match the format of an IPv4 address' - } - } - const [poolName] = useWatch({ control: form.control, name: ['pool'] }) const isPoolSelected = poolName && poolName.length > 0 @@ -163,7 +151,7 @@ export function CreateFloatingIpSideModalForm({ control={form.control} disabled={!isPoolSelected} transform={(ip) => (ip.trim() === '' ? undefined : ip)} - validate={(ip) => validateIpAddress(ip)} + validate={(ip) => (ip && !validateIp(ip).valid ? 'Not a valid IP address' : true)} /> ) From b1c45abac9edebbfbc136a554951336b0139e93b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 9 Feb 2024 14:54:09 -0800 Subject: [PATCH 15/64] Pass address to ip --- libs/api-mocks/msw/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index cf68c19e58..6cf08d701c 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -230,7 +230,7 @@ export const handlers = makeHandlers({ const newFloatingIp: Json = { id: uuid(), project_id: project.id, - ip: '', // 👀 needs a legit ip + ip: body.address || '', // 👀 needs a legit ip ...body, ...getTimestamps(), } From d37b9a77b5a9390cf29e35adafb5036f98ec9428 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 9 Feb 2024 15:56:24 -0800 Subject: [PATCH 16/64] Default to vpcs page --- app/routes.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/routes.tsx b/app/routes.tsx index 0780faaf0a..c7baf45a78 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -322,6 +322,7 @@ export const routes = createRoutesFromElements(
}> + } /> }> Date: Fri, 9 Feb 2024 16:14:54 -0800 Subject: [PATCH 17/64] Proper logic for disabling detach/delete actions --- app/main.tsx | 4 ++-- app/pages/project/networking/FloatingIpsTab.tsx | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/main.tsx b/app/main.tsx index 8c7e77e40d..4cc71bf5c5 100644 --- a/app/main.tsx +++ b/app/main.tsx @@ -6,7 +6,7 @@ * Copyright Oxide Computer Company */ import { QueryClientProvider } from '@tanstack/react-query' -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +// import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { createBrowserRouter, RouterProvider } from 'react-router-dom' @@ -50,7 +50,7 @@ function render() { - + {/* */} ) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 4296ef0154..994983f17d 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -63,7 +63,7 @@ export function FloatingIpsTab() { }) const makeActions = (floatingIp: FloatingIp): MenuAction[] => { - const isAttachedToAnInstance = floatingIp.instanceId !== null + const isAttachedToAnInstance = !!floatingIp.instanceId return [ { label: 'Edit', @@ -82,7 +82,9 @@ export function FloatingIpsTab() { }, { label: 'Detach', - disabled: !isAttachedToAnInstance, + disabled: isAttachedToAnInstance + ? false + : 'This floating IP is not attached to an instance', onActivate() { // Open a modal to attach the floating IP to an instance }, @@ -90,7 +92,9 @@ export function FloatingIpsTab() { { label: 'Delete', // Only available if the floating IP is not attached - disabled: isAttachedToAnInstance, + disabled: isAttachedToAnInstance + ? 'This floating IP must be detached from the instance before it can be deleted' + : false, onActivate: confirmDelete({ doDelete: () => deleteFloatingIp.mutateAsync({ From 591f10ba5cdc90edfc47a87fbc266c1a55363f1e Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 9 Feb 2024 16:20:56 -0800 Subject: [PATCH 18/64] Enable deleting of Floating IPs --- app/pages/project/networking/FloatingIpsTab.tsx | 2 ++ libs/api-mocks/msw/handlers.ts | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 994983f17d..ee8ee79ec8 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -20,6 +20,7 @@ import { buttonStyle, EmptyMessage, Networking24Icon } from '@oxide/ui' import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' import { confirmDelete } from 'app/stores/confirm-delete' +import { addToast } from 'app/stores/toast' import { pb } from 'app/util/path-builder' const EmptyState = () => ( @@ -59,6 +60,7 @@ export function FloatingIpsTab() { const deleteFloatingIp = useApiMutation('floatingIpDelete', { onSuccess() { queryClient.invalidateQueries('floatingIpList') + addToast({ content: 'Your Floating IP has been deleted' }) }, }) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 6cf08d701c..eafd6bbfc2 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -250,6 +250,12 @@ export const handlers = makeHandlers({ )[0] return ip }, + floatingIpDelete({ path, query }) { + const floatingIp = lookup.floatingIp({ ...path, ...query }) + db.floatingIps = db.floatingIps.filter((i) => i.id !== floatingIp.id) + + return 204 + }, imageList({ query }) { if (query.project) { const project = lookup.project(query) @@ -1156,7 +1162,6 @@ export const handlers = makeHandlers({ certificateDelete: NotImplemented, certificateList: NotImplemented, certificateView: NotImplemented, - floatingIpDelete: NotImplemented, floatingIpAttach: NotImplemented, floatingIpDetach: NotImplemented, instanceEphemeralIpDetach: NotImplemented, From 61262c63ecf13c5d8dd8722dc93bd832e7fd18c7 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 12 Feb 2024 10:26:48 -0800 Subject: [PATCH 19/64] FloatingIp sidebar loading, but not saving yet --- app/forms/floating-ip-edit.tsx | 81 +++++++++++++++++++ .../project/networking/FloatingIpsTab.tsx | 2 +- app/routes.tsx | 16 +--- 3 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 app/forms/floating-ip-edit.tsx diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx new file mode 100644 index 0000000000..9d6865eafd --- /dev/null +++ b/app/forms/floating-ip-edit.tsx @@ -0,0 +1,81 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' + +import { apiQueryClient, usePrefetchedApiQuery, type FloatingIp } from '@oxide/api' +import { Networking16Icon, ResourceLabel } from '@oxide/ui' + +import { DescriptionField, NameField, SideModalForm } from 'app/components/form' +import { getFloatingIpSelector, useFloatingIpSelector, useForm } from 'app/hooks' +import { pb } from 'app/util/path-builder' + +// ROUGH EDGE: Trying to get this working, in sidebar +// This is copied off of the Image edit form, but it's not working yet + +EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { + const { project, floatingIp } = getFloatingIpSelector(params) + await apiQueryClient.prefetchQuery('floatingIpView', { + path: { floatingIp }, + query: { project }, + }) + return null +} + +export function EditFloatingIpSideModalForm() { + const { project, floatingIp } = useFloatingIpSelector() + const { data } = usePrefetchedApiQuery('floatingIpView', { + path: { floatingIp }, + query: { project }, + }) + + const dismissLink = pb.floatingIps({ project }) + return +} + +EditSiloImageSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { + const { floatingIp } = getFloatingIpSelector(params) + await apiQueryClient.prefetchQuery('floatingIpView', { path: { floatingIp } }) + return null +} + +export function EditSiloImageSideModalForm() { + const { floatingIp } = useFloatingIpSelector() + const { data } = usePrefetchedApiQuery('floatingIpView', { path: { floatingIp } }) + + return +} + +export function EditImageSideModalForm({ + floatingIp, + dismissLink, +}: { + floatingIp: FloatingIp + dismissLink: string +}) { + const navigate = useNavigate() + const form = useForm({ defaultValues: floatingIp }) + + return ( + navigate(dismissLink)} + subtitle={ + + {floatingIp.name} + + } + // TODO: pass actual error when this form is hooked up + submitError={null} + > + + + + ) +} diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index ee8ee79ec8..96b1a27698 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -134,7 +134,7 @@ export function FloatingIpsTab() { } makeActions={makeActions}> pb.floatingIp({ project, floatingIp }))} + cell={linkCell((floatingIp) => pb.floatingIpEdit({ project, floatingIp }))} /> diff --git a/app/routes.tsx b/app/routes.tsx index c7baf45a78..d4efcaa272 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -11,6 +11,7 @@ import { RouterDataErrorBoundary } from './components/ErrorBoundary' import { NotFound } from './components/ErrorPage' import { CreateDiskSideModalForm } from './forms/disk-create' import { CreateFloatingIpSideModalForm } from './forms/floating-ip-create' +import { EditFloatingIpSideModalForm } from './forms/floating-ip-edit' import { CreateIdpSideModalForm } from './forms/idp/create' import { EditIdpSideModalForm } from './forms/idp/edit' import { @@ -59,7 +60,6 @@ import { ConnectTab } from './pages/project/instances/instance/tabs/ConnectTab' import { MetricsTab } from './pages/project/instances/instance/tabs/MetricsTab' import { NetworkingTab } from './pages/project/instances/instance/tabs/NetworkingTab' import { StorageTab } from './pages/project/instances/instance/tabs/StorageTab' -import { FloatingIpPage } from './pages/project/networking/FloatingIpPage' import { FloatingIpsTab } from './pages/project/networking/FloatingIpsTab' import { ProjectNetworkingPage } from './pages/project/networking/ProjectNetworkingPage' import ProjectsPage from './pages/ProjectsPage' @@ -345,12 +345,12 @@ export const routes = createRoutesFromElements( element={} handle={{ crumb: 'New Floating IP' }} /> - {/* } loader={EditFloatingIpSideModalForm.loader} - handle={{ crumb: 'Edit Floating IP' }} - /> */} + handle={{ crumb: floatingIpCrumb }} + /> @@ -363,14 +363,6 @@ export const routes = createRoutesFromElements( handle={{ crumb: vpcCrumb }} /> - - } - loader={FloatingIpPage.loader} - handle={{ crumb: floatingIpCrumb }} - /> - } loader={DisksPage.loader}> From 22b592711063547a061bb67049a58a5ecc2caf85 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 12 Feb 2024 11:27:04 -0800 Subject: [PATCH 20/64] Refactor --- app/forms/floating-ip-edit.tsx | 71 ++++++++++++++++++---------------- app/routes.tsx | 1 + 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx index 9d6865eafd..5a9ab62a46 100644 --- a/app/forms/floating-ip-edit.tsx +++ b/app/forms/floating-ip-edit.tsx @@ -7,8 +7,15 @@ */ import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, usePrefetchedApiQuery, type FloatingIp } from '@oxide/api' -import { Networking16Icon, ResourceLabel } from '@oxide/ui' +import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' +import { + FormDivider, + Networking16Icon, + PropertiesTable, + ResourceLabel, + Truncate, +} from '@oxide/ui' +import { formatDateTime } from '@oxide/util' import { DescriptionField, NameField, SideModalForm } from 'app/components/form' import { getFloatingIpSelector, useFloatingIpSelector, useForm } from 'app/hooks' @@ -19,44 +26,26 @@ import { pb } from 'app/util/path-builder' EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { const { project, floatingIp } = getFloatingIpSelector(params) - await apiQueryClient.prefetchQuery('floatingIpView', { - path: { floatingIp }, - query: { project }, - }) + await Promise.all([ + apiQueryClient.prefetchQuery('floatingIpView', { + path: { floatingIp }, + query: { project }, + }), + apiQueryClient.prefetchQuery('instanceList', { + query: { project }, + }), + ]) return null } export function EditFloatingIpSideModalForm() { - const { project, floatingIp } = useFloatingIpSelector() - const { data } = usePrefetchedApiQuery('floatingIpView', { - path: { floatingIp }, + const { project, floatingIp: floatingIpName } = useFloatingIpSelector() + const { data: floatingIp } = usePrefetchedApiQuery('floatingIpView', { + path: { floatingIp: floatingIpName }, query: { project }, }) const dismissLink = pb.floatingIps({ project }) - return -} - -EditSiloImageSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { - const { floatingIp } = getFloatingIpSelector(params) - await apiQueryClient.prefetchQuery('floatingIpView', { path: { floatingIp } }) - return null -} - -export function EditSiloImageSideModalForm() { - const { floatingIp } = useFloatingIpSelector() - const { data } = usePrefetchedApiQuery('floatingIpView', { path: { floatingIp } }) - - return -} - -export function EditImageSideModalForm({ - floatingIp, - dismissLink, -}: { - floatingIp: FloatingIp - dismissLink: string -}) { const navigate = useNavigate() const form = useForm({ defaultValues: floatingIp }) @@ -74,8 +63,22 @@ export function EditImageSideModalForm({ // TODO: pass actual error when this form is hooked up submitError={null} > - - + + {floatingIp.name} + + + + {floatingIp.ip} + + {formatDateTime(floatingIp.timeCreated)} + + + + {/* TODO: Add a dropdown for attaching to an instance */} + + + + ) } diff --git a/app/routes.tsx b/app/routes.tsx index d4efcaa272..9954cf23bc 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -354,6 +354,7 @@ export const routes = createRoutesFromElements( + {/* Individual VPC pages don't include top-level VPC navigation */} Date: Mon, 12 Feb 2024 15:04:07 -0800 Subject: [PATCH 21/64] working form in side modal to attach to instances --- app/forms/floating-ip-edit.tsx | 69 +++++++++++++++++++++++++++------- libs/api-mocks/msw/handlers.ts | 12 +++++- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx index 5a9ab62a46..60fc411e50 100644 --- a/app/forms/floating-ip-edit.tsx +++ b/app/forms/floating-ip-edit.tsx @@ -7,9 +7,14 @@ */ import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' import { - FormDivider, + apiQueryClient, + useApiMutation, + useApiQueryClient, + usePrefetchedApiQuery, +} from '@oxide/api' +import { + Listbox, Networking16Icon, PropertiesTable, ResourceLabel, @@ -17,13 +22,11 @@ import { } from '@oxide/ui' import { formatDateTime } from '@oxide/util' -import { DescriptionField, NameField, SideModalForm } from 'app/components/form' +import { SideModalForm } from 'app/components/form' import { getFloatingIpSelector, useFloatingIpSelector, useForm } from 'app/hooks' +import { addToast } from 'app/stores/toast' import { pb } from 'app/util/path-builder' -// ROUGH EDGE: Trying to get this working, in sidebar -// This is copied off of the Image edit form, but it's not working yet - EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { const { project, floatingIp } = getFloatingIpSelector(params) await Promise.all([ @@ -34,6 +37,9 @@ EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { apiQueryClient.prefetchQuery('instanceList', { query: { project }, }), + apiQueryClient.prefetchQuery('floatingIpList', { + query: { project }, + }), ]) return null } @@ -44,17 +50,47 @@ export function EditFloatingIpSideModalForm() { path: { floatingIp: floatingIpName }, query: { project }, }) + const { data: instances } = usePrefetchedApiQuery('instanceList', { + query: { project }, + }) + const { data: floatingIps } = usePrefetchedApiQuery('floatingIpList', { + query: { project }, + }) + console.log(instances, floatingIps) const dismissLink = pb.floatingIps({ project }) const navigate = useNavigate() - const form = useForm({ defaultValues: floatingIp }) + const form = useForm({ defaultValues: { instanceId: floatingIp.instanceId } }) + const onDismiss = () => navigate(dismissLink) + + const queryClient = useApiQueryClient() + + const updateAttachment = useApiMutation('floatingIpAttach', { + onSuccess() { + addToast({ content: 'Floating IP attached' }) + queryClient.invalidateQueries('floatingIpView') + queryClient.invalidateQueries('floatingIpList') + onDismiss() + }, + }) return ( navigate(dismissLink)} + onSubmit={(values) => { + updateAttachment.mutate({ + path: { floatingIp: floatingIpName }, + query: { project }, + body: { + kind: 'instance', + parent: values.instanceId, + }, + }) + }} + submitLabel="Attach" + onDismiss={onDismiss} subtitle={ {floatingIp.name} @@ -69,16 +105,23 @@ export function EditFloatingIpSideModalForm() { {floatingIp.ip} + + <>{instances.items.find((i) => i.id === floatingIp.instanceId)?.name || '–'} + {formatDateTime(floatingIp.timeCreated)} - {/* TODO: Add a dropdown for attaching to an instance */} - - - - + ({ value: i.id, label: i.name }))} + label="Instance" + onChange={(e) => { + form.setValue('instanceId', e) + }} + selected={form.watch('instanceId')} + /> ) } diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index eafd6bbfc2..5a3823ec9c 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -256,6 +256,17 @@ export const handlers = makeHandlers({ return 204 }, + floatingIpAttach({ body, path, query }) { + const floatingIp = lookup.floatingIp({ ...path, ...query }) + const instance = lookup.instance({ instance: body.parent }) + console.log(instance) + + floatingIp.instance_id = instance.id + + console.log(floatingIp) + + return floatingIp + }, imageList({ query }) { if (query.project) { const project = lookup.project(query) @@ -1162,7 +1173,6 @@ export const handlers = makeHandlers({ certificateDelete: NotImplemented, certificateList: NotImplemented, certificateView: NotImplemented, - floatingIpAttach: NotImplemented, floatingIpDetach: NotImplemented, instanceEphemeralIpDetach: NotImplemented, instanceEphemeralIpAttach: NotImplemented, From 7bf2bdce48ae4acc41a26d110b24794ecd7e2142 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 13 Feb 2024 12:40:15 -0800 Subject: [PATCH 22/64] Cleaning up Floating IPs page --- .../FloatingIpPage/FloatingIpPage.tsx | 69 --------------- .../networking/FloatingIpPage/index.tsx | 9 -- .../project/networking/FloatingIpsTab.tsx | 85 +++++++++++-------- app/util/path-builder.ts | 3 - 4 files changed, 49 insertions(+), 117 deletions(-) delete mode 100644 app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx delete mode 100644 app/pages/project/networking/FloatingIpPage/index.tsx diff --git a/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx b/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx deleted file mode 100644 index 177c5f6ff6..0000000000 --- a/app/pages/project/networking/FloatingIpPage/FloatingIpPage.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import type { LoaderFunctionArgs } from 'react-router-dom' - -import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' -import { Networking24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui' -import { formatDateTime } from '@oxide/util' - -import { getFloatingIpSelector, useFloatingIpSelector } from 'app/hooks' - -FloatingIpPage.loader = async ({ params }: LoaderFunctionArgs) => { - const { project, floatingIp } = getFloatingIpSelector(params) - await Promise.all([ - // fetch all instances so we can map their id to their name - apiQueryClient.prefetchQuery('instanceList', { query: { project } }), - - // const { data } = usePrefetchedApiQuery('imageView', { path: { image } }) - - apiQueryClient.prefetchQuery('floatingIpView', { - path: { floatingIp }, - query: { project }, - }), - ]) - return null -} - -export function FloatingIpPage() { - // get the project name from the url - const { project, floatingIp } = useFloatingIpSelector() - // set the instances to the data from the instanceList query and console log them - const { data: instances } = usePrefetchedApiQuery('instanceList', { query: { project } }) - console.log(instances) - // get the floatingIp data - // const { data } = usePrefetchedApiQuery('imageView', { path: { image } }) - const { data: fip } = usePrefetchedApiQuery('floatingIpView', { - path: { floatingIp }, - query: { project }, - }) - console.log(fip) - return ( - <> - - }>{fip.name} - - - - {fip.description} - - - - {formatDateTime(fip.timeCreated)} - - - {formatDateTime(fip.timeModified)} - - - -

- We, uh, don’t actually need a page for this; this was more of a proof-of-concept as - I figured out routing and data fetching -

- - ) -} diff --git a/app/pages/project/networking/FloatingIpPage/index.tsx b/app/pages/project/networking/FloatingIpPage/index.tsx deleted file mode 100644 index f4610d910a..0000000000 --- a/app/pages/project/networking/FloatingIpPage/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ - -export * from './FloatingIpPage' diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 96b1a27698..8d72a81983 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -5,8 +5,8 @@ * * Copyright Oxide Computer Company */ -import { useMemo } from 'react' -import { Link, Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' +import { useState } from 'react' +import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom' import { apiQueryClient, @@ -15,10 +15,10 @@ import { usePrefetchedApiQuery, type FloatingIp, } from '@oxide/api' -import { linkCell, useQueryTable, type MenuAction } from '@oxide/table' -import { buttonStyle, EmptyMessage, Networking24Icon } from '@oxide/ui' +import { useQueryTable, type MenuAction } from '@oxide/table' +import { buttonStyle, EmptyMessage, Modal, Networking24Icon } from '@oxide/ui' -import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' +import { getProjectSelector, useProjectSelector } from 'app/hooks' import { confirmDelete } from 'app/stores/confirm-delete' import { addToast } from 'app/stores/toast' import { pb } from 'app/util/path-builder' @@ -47,15 +47,13 @@ FloatingIpsTab.loader = async ({ params }: LoaderFunctionArgs) => { } export function FloatingIpsTab() { + const [attachModalOpen, setAttachModalOpen] = useState(true) + const [detachModalOpen, setDetachModalOpen] = useState(false) const queryClient = useApiQueryClient() const { project } = useProjectSelector() - const { data: floatingIps } = usePrefetchedApiQuery('floatingIpList', { - query: { project, limit: 25 }, // to have same params as QueryTable - }) const { data: instances } = usePrefetchedApiQuery('instanceList', { query: { project }, }) - const navigate = useNavigate() const deleteFloatingIp = useApiMutation('floatingIpDelete', { onSuccess() { @@ -67,19 +65,13 @@ export function FloatingIpsTab() { const makeActions = (floatingIp: FloatingIp): MenuAction[] => { const isAttachedToAnInstance = !!floatingIp.instanceId return [ - { - label: 'Edit', - onActivate() { - navigate(pb.floatingIpEdit({ project, floatingIp: floatingIp.name }), { - state: floatingIp, - }) - }, - }, { label: 'Attach', - // this should be available even if the floating IP is already attached + disabled: isAttachedToAnInstance + ? 'This floating IP must be detached from the existing instance before it can be attached to a new one' + : false, onActivate() { - // Open a modal to attach the floating IP to an instance + setAttachModalOpen(true) }, }, { @@ -88,12 +80,11 @@ export function FloatingIpsTab() { ? false : 'This floating IP is not attached to an instance', onActivate() { - // Open a modal to attach the floating IP to an instance + setDetachModalOpen(true) }, }, { label: 'Delete', - // Only available if the floating IP is not attached disabled: isAttachedToAnInstance ? 'This floating IP must be detached from the instance before it can be deleted' : false, @@ -109,17 +100,6 @@ export function FloatingIpsTab() { ] } - useQuickActions( - useMemo( - () => - floatingIps.items.map((v) => ({ - value: v.name, - onSelect: () => navigate(pb.floatingIp({ project, floatingIp: v.name })), - navGroup: 'Go to Floating IP', - })), - [project, floatingIps, navigate] - ) - ) const getInstanceName = (instanceId: string) => instances.items.find((i) => i.id === instanceId)?.name @@ -132,10 +112,7 @@ export function FloatingIpsTab() {
} makeActions={makeActions}> - pb.floatingIpEdit({ project, floatingIp }))} - /> +
+ {attachModalOpen && ( + setAttachModalOpen(false)} /> + )} + {detachModalOpen && ( + setDetachModalOpen(false)} /> + )} ) } + +const AttachFloatingIpModal = ({ onDismiss }: { onDismiss: () => void }) => { + return ( + + + womp womp 1 + + alert('hi')} + onDismiss={onDismiss} + > + + ) +} + +const DetachFloatingIpModal = ({ onDismiss }: { onDismiss: () => void }) => { + return ( + + + womp womp 2 + + alert('hi')} + onDismiss={onDismiss} + > + + ) +} diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 6aa1a75087..e53465f260 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -20,7 +20,6 @@ type Image = Required type Snapshot = Required type SiloImage = Required type IpPool = Required -type FloatingIp = Required export const pb = { projects: () => `/projects`, @@ -70,8 +69,6 @@ export const pb = { vpcEdit: (params: Vpc) => `${pb.vpc(params)}/edit`, floatingIps: (params: Project) => `${pb.projectNetworking(params)}/floating-ips`, floatingIpNew: (params: Project) => `${pb.projectNetworking(params)}/floating-ips-new`, - floatingIp: (params: FloatingIp) => `${pb.floatingIps(params)}/${params.floatingIp}`, - floatingIpEdit: (params: FloatingIp) => `${pb.floatingIp(params)}/edit`, siloUtilization: () => '/utilization', siloAccess: () => '/access', From 89e5125e992fbd47ecd9dae30b58feb81cee7838 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 14 Feb 2024 11:21:40 -0800 Subject: [PATCH 23/64] Detaching via modal works --- .../project/networking/FloatingIpsTab.tsx | 71 +++++++++++++++---- libs/api-mocks/msw/handlers.ts | 13 +++- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 8d72a81983..3db602b410 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -47,8 +47,9 @@ FloatingIpsTab.loader = async ({ params }: LoaderFunctionArgs) => { } export function FloatingIpsTab() { - const [attachModalOpen, setAttachModalOpen] = useState(true) + const [attachModalOpen, setAttachModalOpen] = useState(false) const [detachModalOpen, setDetachModalOpen] = useState(false) + const [floatingIpToModify, setFloatingIpToModify] = useState(null) const queryClient = useApiQueryClient() const { project } = useProjectSelector() const { data: instances } = usePrefetchedApiQuery('instanceList', { @@ -71,6 +72,7 @@ export function FloatingIpsTab() { ? 'This floating IP must be detached from the existing instance before it can be attached to a new one' : false, onActivate() { + setFloatingIpToModify(floatingIp) setAttachModalOpen(true) }, }, @@ -80,6 +82,7 @@ export function FloatingIpsTab() { ? false : 'This floating IP is not attached to an instance', onActivate() { + setFloatingIpToModify(floatingIp) setDetachModalOpen(true) }, }, @@ -88,14 +91,17 @@ export function FloatingIpsTab() { disabled: isAttachedToAnInstance ? 'This floating IP must be detached from the instance before it can be deleted' : false, - onActivate: confirmDelete({ - doDelete: () => - deleteFloatingIp.mutateAsync({ - path: { floatingIp: floatingIp.name }, - query: { project }, - }), - label: floatingIp.name, - }), + onActivate: () => { + confirmDelete({ + doDelete: () => + deleteFloatingIp.mutateAsync({ + path: { floatingIp: floatingIp.name }, + query: { project }, + }), + label: floatingIp.name, + }), + setFloatingIpToModify(null) + }, }, ] } @@ -117,7 +123,7 @@ export function FloatingIpsTab() { getInstanceName(instanceId)} /> @@ -125,8 +131,13 @@ export function FloatingIpsTab() { {attachModalOpen && ( setAttachModalOpen(false)} /> )} - {detachModalOpen && ( - setDetachModalOpen(false)} /> + {detachModalOpen && floatingIpToModify?.instanceId && ( + setDetachModalOpen(false)} + floatingIpName={floatingIpToModify.name} + instanceName={getInstanceName(floatingIpToModify.instanceId) || 'this instance'} + projectName={project} + /> )} ) @@ -147,15 +158,45 @@ const AttachFloatingIpModal = ({ onDismiss }: { onDismiss: () => void }) => { ) } -const DetachFloatingIpModal = ({ onDismiss }: { onDismiss: () => void }) => { +const DetachFloatingIpModal = ({ + floatingIpName, + instanceName, + onDismiss, + projectName, +}: { + floatingIpName: string + instanceName: string + onDismiss: () => void + projectName: string +}) => { + const queryClient = useApiQueryClient() + const floatingIpDetach = useApiMutation('floatingIpDetach', { + onSuccess() { + queryClient.invalidateQueries('floatingIpList') + addToast({ content: 'Your Floating IP has been detached' }) + onDismiss() + }, + onError: (err) => { + addToast({ title: 'Error', content: err.message, variant: 'error' }) + }, + }) return ( - womp womp 2 + + Detach {floatingIpName} from {instanceName}? + alert('hi')} + onAction={() => + floatingIpName + ? floatingIpDetach.mutate({ + path: { floatingIp: floatingIpName }, + query: { project: projectName }, + }) + : undefined + } onDismiss={onDismiss} > diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 5a3823ec9c..b67a990cae 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -16,6 +16,7 @@ import { INSTANCE_MIN_RAM_GiB, MAX_NICS_PER_INSTANCE, type ApiTypes as Api, + type FloatingIp, type SamlIdentityProvider, } from '@oxide/api' import { json, makeHandlers, type Json } from '@oxide/gen/msw-handlers' @@ -267,6 +268,17 @@ export const handlers = makeHandlers({ return floatingIp }, + floatingIpDetach({ path, query }) { + const floatingIp: FloatingIp = lookup.floatingIp({ ...path, ...query }) + db.floatingIps = db.floatingIps.map((ip) => { + if (ip.id !== floatingIp.id) { + return ip + } + return { ...ip, instance_id: undefined } + }) + + return 204 + }, imageList({ query }) { if (query.project) { const project = lookup.project(query) @@ -1173,7 +1185,6 @@ export const handlers = makeHandlers({ certificateDelete: NotImplemented, certificateList: NotImplemented, certificateView: NotImplemented, - floatingIpDetach: NotImplemented, instanceEphemeralIpDetach: NotImplemented, instanceEphemeralIpAttach: NotImplemented, instanceMigrate: NotImplemented, From 0662b94d8cf8134a08c426611840841a4200be07 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 14 Feb 2024 11:48:28 -0800 Subject: [PATCH 24/64] Refactor --- .../project/networking/FloatingIpsTab.tsx | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 3db602b410..125ef2f55b 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -133,10 +133,10 @@ export function FloatingIpsTab() { )} {detachModalOpen && floatingIpToModify?.instanceId && ( setDetachModalOpen(false)} - floatingIpName={floatingIpToModify.name} - instanceName={getInstanceName(floatingIpToModify.instanceId) || 'this instance'} - projectName={project} /> )} @@ -147,7 +147,7 @@ const AttachFloatingIpModal = ({ onDismiss }: { onDismiss: () => void }) => { return ( - womp womp 1 + Select an instance to attach $name to void }) => { } const DetachFloatingIpModal = ({ - floatingIpName, - instanceName, + floatingIp, + instance, + project, onDismiss, - projectName, }: { - floatingIpName: string - instanceName: string + floatingIp: string + instance: string + project: string onDismiss: () => void - projectName: string }) => { const queryClient = useApiQueryClient() const floatingIpDetach = useApiMutation('floatingIpDetach', { @@ -184,18 +184,13 @@ const DetachFloatingIpModal = ({ - Detach {floatingIpName} from {instanceName}? + Detach {floatingIp} from {instance}? - floatingIpName - ? floatingIpDetach.mutate({ - path: { floatingIp: floatingIpName }, - query: { project: projectName }, - }) - : undefined + floatingIpDetach.mutate({ path: { floatingIp }, query: { project } }) } onDismiss={onDismiss} > From 6cd38836da2cdf2b179f9e1e63594175b8f03fe1 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 14 Feb 2024 12:10:55 -0800 Subject: [PATCH 25/64] Attach Floating IP to Instance working --- .../project/networking/FloatingIpsTab.tsx | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index 125ef2f55b..cddf5cc21e 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -6,6 +6,7 @@ * Copyright Oxide Computer Company */ import { useState } from 'react' +import { useForm } from 'react-hook-form' import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom' import { @@ -14,9 +15,10 @@ import { useApiQueryClient, usePrefetchedApiQuery, type FloatingIp, + type Instance, } from '@oxide/api' import { useQueryTable, type MenuAction } from '@oxide/table' -import { buttonStyle, EmptyMessage, Modal, Networking24Icon } from '@oxide/ui' +import { buttonStyle, EmptyMessage, Listbox, Modal, Networking24Icon } from '@oxide/ui' import { getProjectSelector, useProjectSelector } from 'app/hooks' import { confirmDelete } from 'app/stores/confirm-delete' @@ -128,8 +130,13 @@ export function FloatingIpsTab() { /> - {attachModalOpen && ( - setAttachModalOpen(false)} /> + {attachModalOpen && floatingIpToModify && ( + setAttachModalOpen(false)} + /> )} {detachModalOpen && floatingIpToModify?.instanceId && ( void }) => { +const AttachFloatingIpModal = ({ + floatingIp, + instances, + project, + onDismiss, +}: { + floatingIp: string + instances: Array + project: string + onDismiss: () => void +}) => { + const queryClient = useApiQueryClient() + const floatingIpAttach = useApiMutation('floatingIpAttach', { + onSuccess() { + queryClient.invalidateQueries('floatingIpList') + addToast({ content: 'Your Floating IP has been attached' }) + onDismiss() + }, + onError: (err) => { + addToast({ title: 'Error', content: err.message, variant: 'error' }) + }, + }) + const form = useForm({ defaultValues: { instanceId: '' } }) + return ( - Select an instance to attach $name to + +
+ ({ value: i.id, label: i.name }))} + label="Select an instance" + onChange={(e) => { + form.setValue('instanceId', e) + }} + selected={form.watch('instanceId')} + /> + +
alert('hi')} + disabled={!form.getValues('instanceId')} + onAction={() => + floatingIpAttach.mutate({ + path: { floatingIp }, + query: { project }, + body: { kind: 'instance', parent: form.getValues('instanceId') }, + }) + } onDismiss={onDismiss} >
From 421a144e119bea4b2b09ef0d8a423e743de57a9a Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 14 Feb 2024 14:06:53 -0800 Subject: [PATCH 26/64] Spoof ip --- libs/api-mocks/msw/handlers.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index b67a990cae..f50aacddbc 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -231,14 +231,13 @@ export const handlers = makeHandlers({ const newFloatingIp: Json = { id: uuid(), project_id: project.id, - ip: body.address || '', // 👀 needs a legit ip + ip: body.address || '12.34.56.7', ...body, ...getTimestamps(), } db.floatingIps.push(newFloatingIp) return json(newFloatingIp, { status: 201 }) }, - floatingIpList({ query }) { const project = lookup.project(query) const ips = db.floatingIps.filter((i) => i.project_id === project.id) From 70c80f5212fb4a7d838b01a96e9addd90f98bc5a Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 14 Feb 2024 15:38:56 -0800 Subject: [PATCH 27/64] Resolve TS issues --- app/forms/floating-ip-edit.tsx | 127 - app/routes.tsx | 8 - libs/api-mocks/disk.js | 140 ++ libs/api-mocks/external-ip.js | 37 + libs/api-mocks/floating-ip.js | 34 + libs/api-mocks/image.js | 30 + libs/api-mocks/index.js | 50 + libs/api-mocks/instance.js | 44 + libs/api-mocks/ip-pool.js | 74 + libs/api-mocks/json-type.js | 12 + libs/api-mocks/metrics.js | 37 + libs/api-mocks/msw/db.js | 307 +++ libs/api-mocks/msw/handlers.js | 1107 +++++++++ libs/api-mocks/msw/handlers.ts | 26 +- libs/api-mocks/msw/util.js | 314 +++ libs/api-mocks/network-interface.js | 18 + libs/api-mocks/physical-disk.js | 45 + libs/api-mocks/project.js | 28 + libs/api-mocks/rack.js | 9 + libs/api-mocks/role-assignment.js | 52 + libs/api-mocks/serial.js | 3480 +++++++++++++++++++++++++++ libs/api-mocks/silo.js | 96 + libs/api-mocks/sled.js | 18 + libs/api-mocks/snapshot.js | 95 + libs/api-mocks/sshKeys.js | 24 + libs/api-mocks/user-group.js | 31 + libs/api-mocks/user.js | 25 + libs/api-mocks/vpc.js | 117 + 28 files changed, 6233 insertions(+), 152 deletions(-) delete mode 100644 app/forms/floating-ip-edit.tsx create mode 100644 libs/api-mocks/disk.js create mode 100644 libs/api-mocks/external-ip.js create mode 100644 libs/api-mocks/floating-ip.js create mode 100644 libs/api-mocks/image.js create mode 100644 libs/api-mocks/index.js create mode 100644 libs/api-mocks/instance.js create mode 100644 libs/api-mocks/ip-pool.js create mode 100644 libs/api-mocks/json-type.js create mode 100644 libs/api-mocks/metrics.js create mode 100644 libs/api-mocks/msw/db.js create mode 100644 libs/api-mocks/msw/handlers.js create mode 100644 libs/api-mocks/msw/util.js create mode 100644 libs/api-mocks/network-interface.js create mode 100644 libs/api-mocks/physical-disk.js create mode 100644 libs/api-mocks/project.js create mode 100644 libs/api-mocks/rack.js create mode 100644 libs/api-mocks/role-assignment.js create mode 100644 libs/api-mocks/serial.js create mode 100644 libs/api-mocks/silo.js create mode 100644 libs/api-mocks/sled.js create mode 100644 libs/api-mocks/snapshot.js create mode 100644 libs/api-mocks/sshKeys.js create mode 100644 libs/api-mocks/user-group.js create mode 100644 libs/api-mocks/user.js create mode 100644 libs/api-mocks/vpc.js diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx deleted file mode 100644 index 60fc411e50..0000000000 --- a/app/forms/floating-ip-edit.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' - -import { - apiQueryClient, - useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, -} from '@oxide/api' -import { - Listbox, - Networking16Icon, - PropertiesTable, - ResourceLabel, - Truncate, -} from '@oxide/ui' -import { formatDateTime } from '@oxide/util' - -import { SideModalForm } from 'app/components/form' -import { getFloatingIpSelector, useFloatingIpSelector, useForm } from 'app/hooks' -import { addToast } from 'app/stores/toast' -import { pb } from 'app/util/path-builder' - -EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { - const { project, floatingIp } = getFloatingIpSelector(params) - await Promise.all([ - apiQueryClient.prefetchQuery('floatingIpView', { - path: { floatingIp }, - query: { project }, - }), - apiQueryClient.prefetchQuery('instanceList', { - query: { project }, - }), - apiQueryClient.prefetchQuery('floatingIpList', { - query: { project }, - }), - ]) - return null -} - -export function EditFloatingIpSideModalForm() { - const { project, floatingIp: floatingIpName } = useFloatingIpSelector() - const { data: floatingIp } = usePrefetchedApiQuery('floatingIpView', { - path: { floatingIp: floatingIpName }, - query: { project }, - }) - const { data: instances } = usePrefetchedApiQuery('instanceList', { - query: { project }, - }) - const { data: floatingIps } = usePrefetchedApiQuery('floatingIpList', { - query: { project }, - }) - console.log(instances, floatingIps) - - const dismissLink = pb.floatingIps({ project }) - const navigate = useNavigate() - const form = useForm({ defaultValues: { instanceId: floatingIp.instanceId } }) - const onDismiss = () => navigate(dismissLink) - - const queryClient = useApiQueryClient() - - const updateAttachment = useApiMutation('floatingIpAttach', { - onSuccess() { - addToast({ content: 'Floating IP attached' }) - queryClient.invalidateQueries('floatingIpView') - queryClient.invalidateQueries('floatingIpList') - onDismiss() - }, - }) - - return ( - { - updateAttachment.mutate({ - path: { floatingIp: floatingIpName }, - query: { project }, - body: { - kind: 'instance', - parent: values.instanceId, - }, - }) - }} - submitLabel="Attach" - onDismiss={onDismiss} - subtitle={ - - {floatingIp.name} - - } - // TODO: pass actual error when this form is hooked up - submitError={null} - > - - {floatingIp.name} - - - - {floatingIp.ip} - - <>{instances.items.find((i) => i.id === floatingIp.instanceId)?.name || '–'} - - - {formatDateTime(floatingIp.timeCreated)} - - - - ({ value: i.id, label: i.name }))} - label="Instance" - onChange={(e) => { - form.setValue('instanceId', e) - }} - selected={form.watch('instanceId')} - /> - - ) -} diff --git a/app/routes.tsx b/app/routes.tsx index 9954cf23bc..c9cf24298b 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -11,7 +11,6 @@ import { RouterDataErrorBoundary } from './components/ErrorBoundary' import { NotFound } from './components/ErrorPage' import { CreateDiskSideModalForm } from './forms/disk-create' import { CreateFloatingIpSideModalForm } from './forms/floating-ip-create' -import { EditFloatingIpSideModalForm } from './forms/floating-ip-edit' import { CreateIdpSideModalForm } from './forms/idp/create' import { EditIdpSideModalForm } from './forms/idp/edit' import { @@ -83,7 +82,6 @@ import { pb } from './util/path-builder' const projectCrumb: CrumbFunc = (m) => m.params.project! const instanceCrumb: CrumbFunc = (m) => m.params.instance! -const floatingIpCrumb: CrumbFunc = (m) => m.params.floatingIp! const vpcCrumb: CrumbFunc = (m) => m.params.vpc! const siloCrumb: CrumbFunc = (m) => m.params.silo! @@ -345,12 +343,6 @@ export const routes = createRoutesFromElements( element={} handle={{ crumb: 'New Floating IP' }} /> - } - loader={EditFloatingIpSideModalForm.loader} - handle={{ crumb: floatingIpCrumb }} - />
diff --git a/libs/api-mocks/disk.js b/libs/api-mocks/disk.js new file mode 100644 index 0000000000..aaac051e66 --- /dev/null +++ b/libs/api-mocks/disk.js @@ -0,0 +1,140 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.disks = void 0; +var util_1 = require("@oxide/util"); +var instance_1 = require("./instance"); +var project_1 = require("./project"); +exports.disks = [ + { + id: '7f2309a5-13e3-47e0-8a4c-2a3b3bc992fd', + name: 'disk-1', + description: "it's a disk", + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'attached', instance: instance_1.instance.id }, + device_path: '/abc', + size: 2 * util_1.GiB, + block_size: 2048, + }, + { + id: '48f94570-60d8-401c-857f-5bf912d2d3fc', + name: 'disk-2', + description: "it's a second disk", + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'attached', instance: instance_1.instance.id }, + device_path: '/def', + size: 4 * util_1.GiB, + block_size: 2048, + }, + { + id: '3b768903-1d0b-4d78-9308-c12d3889bdfb', + name: 'disk-3', + description: "it's a third disk", + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'detached' }, + device_path: '/ghi', + size: 6 * util_1.GiB, + block_size: 2048, + }, + { + id: '5695b16d-e1d6-44b0-a75c-7b4299831540', + name: 'disk-4', + description: "it's a fourth disk", + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'detached' }, + device_path: '/jkl', + size: 64 * util_1.GiB, + block_size: 2048, + }, + { + id: '4d6f4c76-675f-4cda-b609-f3b8b301addb', + name: 'disk-5', + description: '', + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'detached' }, + device_path: '/jkl', + size: 128 * util_1.GiB, + block_size: 2048, + }, + { + id: '41481936-5a6b-4dcd-8dec-26c3bdc343bd', + name: 'disk-6', + description: '', + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'detached' }, + device_path: '/jkl', + size: 20 * util_1.GiB, + block_size: 2048, + }, + { + id: '704cd392-9f6b-4a2b-8410-1f1e0794db80', + name: 'disk-7', + description: '', + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'detached' }, + device_path: '/jkl', + size: 24 * util_1.GiB, + block_size: 2048, + }, + { + id: '305ee9c7-1930-4a8f-86d7-ed9eece9598e', + name: 'disk-8', + description: '', + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'detached' }, + device_path: '/jkl', + size: 16 * util_1.GiB, + block_size: 2048, + }, + { + id: 'ccad8d48-df21-4a80-8c16-683ee6bfb290', + name: 'disk-9', + description: '', + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'detached' }, + device_path: '/jkl', + size: 32 * util_1.GiB, + block_size: 2048, + }, + { + id: 'a028160f-603c-4562-bb71-d2d76f1ac2a8', + name: 'disk-10', + description: '', + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'detached' }, + device_path: '/jkl', + size: 24 * util_1.GiB, + block_size: 2048, + }, + { + id: '3f23c80f-c523-4d86-8292-2ca3f807bb12', + name: 'disk-snapshot-error', + description: '', + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + state: { state: 'detached' }, + device_path: '/jkl', + size: 12 * util_1.GiB, + block_size: 2048, + }, +]; diff --git a/libs/api-mocks/external-ip.js b/libs/api-mocks/external-ip.js new file mode 100644 index 0000000000..d0d467994d --- /dev/null +++ b/libs/api-mocks/external-ip.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.externalIps = void 0; +var instance_1 = require("./instance"); +// TODO: this type represents the API response, but we need to mock more +// structure in order to be able to look up IPs for a particular instance +exports.externalIps = [ + { + instance_id: instance_1.instances[0].id, + external_ip: { + ip: "123.4.56.0", + kind: 'ephemeral', + }, + }, + // middle one has no IPs + { + instance_id: instance_1.instances[2].id, + external_ip: { + ip: "123.4.56.1", + kind: 'ephemeral', + }, + }, + { + instance_id: instance_1.instances[2].id, + external_ip: { + ip: "123.4.56.2", + kind: 'ephemeral', + }, + }, + { + instance_id: instance_1.instances[2].id, + external_ip: { + ip: "123.4.56.3", + kind: 'ephemeral', + }, + }, +]; diff --git a/libs/api-mocks/floating-ip.js b/libs/api-mocks/floating-ip.js new file mode 100644 index 0000000000..f01a30fe24 --- /dev/null +++ b/libs/api-mocks/floating-ip.js @@ -0,0 +1,34 @@ +"use strict"; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.floatingIps = exports.floatingIp2 = exports.floatingIp = void 0; +var _1 = require("."); +// A floating IP from the default pool +exports.floatingIp = { + id: '3ca0ccb7-d66d-4fde-a871-ab9855eaea8e', + name: 'rootbeer-float', + description: 'A classic.', + instance_id: undefined, + ip: '192.168.32.1', + project_id: _1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), +}; +// A floating IP attached to a particular instance +exports.floatingIp2 = { + id: '0a00a6c3-4821-4bb8-af77-574468ac6651', + name: 'cola-float', + description: 'A favourite.', + instance_id: _1.instance.id, + ip: '192.168.64.64', + project_id: _1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), +}; +exports.floatingIps = [exports.floatingIp, exports.floatingIp2]; diff --git a/libs/api-mocks/image.js b/libs/api-mocks/image.js new file mode 100644 index 0000000000..4251fa2313 --- /dev/null +++ b/libs/api-mocks/image.js @@ -0,0 +1,30 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.images = void 0; +var util_1 = require("@oxide/util"); +var project_1 = require("./project"); +var base = { + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + block_size: 512, +}; +exports.images = [ + __assign({ id: '7ea31aad-7004-4d1e-ada6-a2e447da40b7', name: 'image-1', description: "it's an image", size: 4 * util_1.GiB, os: 'alpine', version: 'edge1', project_id: project_1.project.id }, base), + __assign({ id: '9bbba93d-aac3-4c00-ad04-2e05a555a59a', name: 'image-2', description: "it's a second image", size: 5 * util_1.GiB, os: 'alpine', version: 'edge2', project_id: project_1.project.id }, base), + __assign({ id: '4700ecf1-8f48-4ecf-b78e-816ddb76aaca', name: 'image-3', description: "it's a third image", size: 6 * util_1.GiB, os: 'alpine', version: 'edge3', project_id: project_1.project.id }, base), + __assign({ id: 'd150b87d-eb20-49d2-8b56-ff5564670e8c', name: 'image-4', description: "it's a fourth image", size: 7 * util_1.GiB, os: 'alpine', version: 'edge4', project_id: project_1.project.id }, base), + __assign({ id: 'ae46ddf5-a8d5-40fa-bcda-fcac606e3f9b', name: 'ubuntu-22-04', description: 'Latest Ubuntu LTS', os: 'ubuntu', version: '22.04', size: 1 * util_1.GiB }, base), + __assign({ id: 'a2ea1d7a-cc5a-4fda-a400-e2d2b18f53c5', name: 'ubuntu-20-04', description: 'Previous LTS', os: 'ubuntu', version: '20.04', size: 2 * util_1.GiB }, base), + __assign({ id: 'bd6aa051-8075-421d-a641-fae54a0ce8ef', name: 'arch-2022-06-01', description: 'Latest Arch Linux', os: 'arch', version: '2022.06.01', size: 3 * util_1.GiB }, base), +]; diff --git a/libs/api-mocks/index.js b/libs/api-mocks/index.js new file mode 100644 index 0000000000..cc9c24bc53 --- /dev/null +++ b/libs/api-mocks/index.js @@ -0,0 +1,50 @@ +"use strict"; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resetDb = exports.MSW_USER_COOKIE = exports.json = exports.handlers = void 0; +__exportStar(require("./disk"), exports); +__exportStar(require("./external-ip"), exports); +__exportStar(require("./floating-ip"), exports); +__exportStar(require("./image"), exports); +__exportStar(require("./instance"), exports); +__exportStar(require("./ip-pool"), exports); +__exportStar(require("./network-interface"), exports); +__exportStar(require("./physical-disk"), exports); +__exportStar(require("./project"), exports); +__exportStar(require("./rack"), exports); +__exportStar(require("./role-assignment"), exports); +__exportStar(require("./silo"), exports); +__exportStar(require("./sled"), exports); +__exportStar(require("./snapshot"), exports); +__exportStar(require("./sshKeys"), exports); +__exportStar(require("./user"), exports); +__exportStar(require("./user-group"), exports); +__exportStar(require("./user"), exports); +__exportStar(require("./vpc"), exports); +var handlers_1 = require("./msw/handlers"); +Object.defineProperty(exports, "handlers", { enumerable: true, get: function () { return handlers_1.handlers; } }); +var util_1 = require("./msw/util"); +Object.defineProperty(exports, "json", { enumerable: true, get: function () { return util_1.json; } }); +Object.defineProperty(exports, "MSW_USER_COOKIE", { enumerable: true, get: function () { return util_1.MSW_USER_COOKIE; } }); +var db_1 = require("./msw/db"); +Object.defineProperty(exports, "resetDb", { enumerable: true, get: function () { return db_1.resetDb; } }); diff --git a/libs/api-mocks/instance.js b/libs/api-mocks/instance.js new file mode 100644 index 0000000000..9b2800f4c3 --- /dev/null +++ b/libs/api-mocks/instance.js @@ -0,0 +1,44 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.instances = exports.instance = void 0; +var project_1 = require("./project"); +exports.instance = { + id: '935499b3-fd96-432a-9c21-83a3dc1eece4', + name: 'db1', + ncpus: 7, + memory: 1024 * 1024 * 256, + description: 'an instance', + hostname: 'oxide.com', + project_id: project_1.project.id, + run_state: 'running', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + time_run_state_updated: new Date().toISOString(), +}; +var failedInstance = { + id: 'b5946edc-5bed-4597-88ab-9a8beb9d32a4', + name: 'you-fail', + ncpus: 7, + memory: 1024 * 1024 * 256, + description: 'a failed instance', + hostname: 'oxide.com', + project_id: project_1.project.id, + run_state: 'failed', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + time_run_state_updated: new Date().toISOString(), +}; +var startingInstance = { + id: '16737f54-1f76-4c96-8b7c-9d24971c1d62', + name: 'not-there-yet', + ncpus: 7, + memory: 1024 * 1024 * 256, + description: 'a starting instance', + hostname: 'oxide.com', + project_id: project_1.project.id, + run_state: 'starting', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + time_run_state_updated: new Date().toISOString(), +}; +exports.instances = [exports.instance, failedInstance, startingInstance]; diff --git a/libs/api-mocks/ip-pool.js b/libs/api-mocks/ip-pool.js new file mode 100644 index 0000000000..d00f3261af --- /dev/null +++ b/libs/api-mocks/ip-pool.js @@ -0,0 +1,74 @@ +"use strict"; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ipPoolRanges = exports.ipPoolSilos = exports.ipPools = void 0; +var silo_1 = require("./silo"); +var ipPool1 = { + id: '69b5c583-74a9-451a-823d-0741c1ec66e2', + name: 'ip-pool-1', + description: '', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), +}; +var ipPool2 = { + id: 'af2fbe06-b21d-4364-96b7-a58220bc3242', + name: 'ip-pool-2', + description: '', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), +}; +var ipPool3 = { + id: '8929a9ec-03d7-4027-8bf3-dda76627de07', + name: 'ip-pool-3', + description: '', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), +}; +exports.ipPools = [ipPool1, ipPool2, ipPool3]; +exports.ipPoolSilos = [ + { + ip_pool_id: ipPool1.id, + silo_id: silo_1.defaultSilo.id, + is_default: true, + }, + { + ip_pool_id: ipPool2.id, + silo_id: silo_1.defaultSilo.id, + is_default: false, + }, +]; +exports.ipPoolRanges = [ + { + id: 'bbfcf3f2-061e-4334-a0e7-dfcd8171f87e', + ip_pool_id: ipPool1.id, + range: { + first: '10.0.0.1', + last: '10.0.0.5', + }, + time_created: new Date().toISOString(), + }, + { + id: 'df05795b-cb88-4971-9865-ac2995c2b2d4', + ip_pool_id: ipPool1.id, + range: { + first: '10.0.0.20', + last: '10.0.0.22', + }, + time_created: new Date().toISOString(), + }, + { + id: '7e6e94b9-748e-4219-83a3-cec76253ec70', + ip_pool_id: ipPool2.id, + range: { + first: '10.0.0.33', + last: '10.0.0.38', + }, + time_created: new Date().toISOString(), + }, +]; diff --git a/libs/api-mocks/json-type.js b/libs/api-mocks/json-type.js new file mode 100644 index 0000000000..a764bbeff0 --- /dev/null +++ b/libs/api-mocks/json-type.js @@ -0,0 +1,12 @@ +"use strict"; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Json = void 0; +var msw_handlers_1 = require("@oxide/gen/msw-handlers"); +Object.defineProperty(exports, "Json", { enumerable: true, get: function () { return msw_handlers_1.Json; } }); diff --git a/libs/api-mocks/metrics.js b/libs/api-mocks/metrics.js new file mode 100644 index 0000000000..d2bc3c6921 --- /dev/null +++ b/libs/api-mocks/metrics.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.genI64Data = exports.genCumulativeI64Data = void 0; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +var date_fns_1 = require("date-fns"); +/** evenly distribute the `values` across the time interval */ +var genCumulativeI64Data = function (values, startTime, endTime) { + var intervalSeconds = (0, date_fns_1.differenceInSeconds)(endTime, startTime) / values.length; + return values.map(function (value, i) { return ({ + datum: { + datum: { + value: value, + start_time: startTime.toISOString(), + }, + type: 'cumulative_i64', + }, + timestamp: (0, date_fns_1.addSeconds)(startTime, i * intervalSeconds).toISOString(), + }); }); +}; +exports.genCumulativeI64Data = genCumulativeI64Data; +var genI64Data = function (values, startTime, endTime) { + var intervalSeconds = (0, date_fns_1.differenceInSeconds)(endTime, startTime) / values.length; + return values.map(function (value, i) { return ({ + datum: { + datum: value, + type: 'i64', + }, + timestamp: (0, date_fns_1.addSeconds)(startTime, i * intervalSeconds).toISOString(), + }); }); +}; +exports.genI64Data = genI64Data; diff --git a/libs/api-mocks/msw/db.js b/libs/api-mocks/msw/db.js new file mode 100644 index 0000000000..bf71c217b3 --- /dev/null +++ b/libs/api-mocks/msw/db.js @@ -0,0 +1,307 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resetDb = exports.db = exports.utilizationForSilo = exports.lookup = exports.lookupById = exports.notFoundErr = void 0; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +// note that isUuid checks for any kind of UUID. strictly speaking, we should +// only be checking for v4 +var uuid_1 = require("uuid"); +var mock = require("@oxide/api-mocks"); +var api_mocks_1 = require("@oxide/api-mocks"); +var util_1 = require("@oxide/util"); +var util_2 = require("./util"); +var notFoundBody = { error_code: 'ObjectNotFound' }; +var notFoundErr = function (msg) { + var message = msg ? "not found: ".concat(msg) : 'not found'; + return (0, util_2.json)({ error_code: 'ObjectNotFound', message: message }, { status: 404 }); +}; +exports.notFoundErr = notFoundErr; +var lookupById = function (table, id) { + var item = table.find(function (i) { return i.id === id; }); + if (!item) + throw exports.notFoundErr; + return item; +}; +exports.lookupById = lookupById; +exports.lookup = { + project: function (_a) { + var id = _a.project; + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.projects, id); + var project = exports.db.projects.find(function (p) { return p.name === id; }); + if (!project) + throw exports.notFoundErr; + return project; + }, + instance: function (_a) { + var id = _a.instance, projectSelector = __rest(_a, ["instance"]); + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.instances, id); + var project = exports.lookup.project(projectSelector); + var instance = exports.db.instances.find(function (i) { return i.project_id === project.id && i.name === id; }); + if (!instance) + throw exports.notFoundErr; + return instance; + }, + networkInterface: function (_a) { + var id = _a.interface, instanceSelector = __rest(_a, ["interface"]); + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.networkInterfaces, id); + var instance = exports.lookup.instance(instanceSelector); + var nic = exports.db.networkInterfaces.find(function (n) { return n.instance_id === instance.id && n.name === id; }); + if (!nic) + throw exports.notFoundErr; + return nic; + }, + disk: function (_a) { + var id = _a.disk, projectSelector = __rest(_a, ["disk"]); + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.disks, id); + var project = exports.lookup.project(projectSelector); + var disk = exports.db.disks.find(function (d) { return d.project_id === project.id && d.name === id; }); + if (!disk) + throw exports.notFoundErr; + return disk; + }, + floatingIp: function (_a) { + var id = _a.floatingIp, projectSelector = __rest(_a, ["floatingIp"]); + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.floatingIps, id); + var project = exports.lookup.project(projectSelector); + var floatingIp = exports.db.floatingIps.find(function (i) { return i.project_id === project.id && i.name === id; }); + if (!floatingIp) + throw exports.notFoundErr; + return floatingIp; + }, + snapshot: function (_a) { + var id = _a.snapshot, projectSelector = __rest(_a, ["snapshot"]); + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.snapshots, id); + var project = exports.lookup.project(projectSelector); + var snapshot = exports.db.snapshots.find(function (i) { return i.project_id === project.id && i.name === id; }); + if (!snapshot) + throw exports.notFoundErr; + return snapshot; + }, + vpc: function (_a) { + var id = _a.vpc, projectSelector = __rest(_a, ["vpc"]); + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.vpcs, id); + var project = exports.lookup.project(projectSelector); + var vpc = exports.db.vpcs.find(function (v) { return v.project_id === project.id && v.name === id; }); + if (!vpc) + throw exports.notFoundErr; + return vpc; + }, + vpcSubnet: function (_a) { + var id = _a.subnet, vpcSelector = __rest(_a, ["subnet"]); + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.vpcSubnets, id); + var vpc = exports.lookup.vpc(vpcSelector); + var subnet = exports.db.vpcSubnets.find(function (s) { return s.vpc_id === vpc.id && s.name === id; }); + if (!subnet) + throw exports.notFoundErr; + return subnet; + }, + image: function (_a) { + var id = _a.image, projectId = _a.project; + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.images, id); + var image; + if (projectId === undefined) { + // silo image + image = exports.db.images.find(function (d) { return d.project_id === undefined && d.name === id; }); + } + else { + // project image + var project_1 = exports.lookup.project({ project: projectId }); + image = exports.db.images.find(function (d) { return d.project_id === project_1.id && d.name === id; }); + } + if (!image) + throw exports.notFoundErr; + return image; + }, + ipPool: function (_a) { + var id = _a.pool; + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.ipPools, id); + var pool = exports.db.ipPools.find(function (p) { return p.name === id; }); + if (!pool) + throw exports.notFoundErr; + return pool; + }, + // unusual one because it's a sibling relationship. we look up both the pool and the silo first + ipPoolSiloLink: function (_a) { + var poolId = _a.pool, siloId = _a.silo; + var pool = exports.lookup.ipPool({ pool: poolId }); + var silo = exports.lookup.silo({ silo: siloId }); + var ipPoolSilo = exports.db.ipPoolSilos.find(function (ips) { return ips.ip_pool_id === pool.id && ips.silo_id === silo.id; }); + if (!ipPoolSilo) + throw exports.notFoundErr; + return ipPoolSilo; + }, + // unusual because it returns a list, but we need it for multiple endpoints + siloIpPools: function (path) { + var silo = exports.lookup.silo(path); + // effectively join db.ipPools and db.ipPoolSilos on ip_pool_id + return exports.db.ipPoolSilos + .filter(function (link) { return link.silo_id === silo.id; }) + .map(function (link) { + var pool = exports.db.ipPools.find(function (pool) { return pool.id === link.ip_pool_id; }); + // this should never happen + if (!pool) { + var linkStr = JSON.stringify(link); + var message = "Found IP pool-silo link without corresponding pool: ".concat(linkStr); + throw (0, util_2.json)({ message: message }, { status: 500 }); + } + return __assign(__assign({}, pool), { is_default: link.is_default }); + }); + }, + samlIdp: function (_a) { + var id = _a.provider, siloSelector = __rest(_a, ["provider"]); + if (!id) + throw exports.notFoundErr; + var silo = exports.lookup.silo(siloSelector); + var dbIdp = exports.db.identityProviders.find(function (_a) { + var type = _a.type, siloId = _a.siloId, provider = _a.provider; + return type === 'saml' && siloId === silo.id && provider.name === id; + }); + if (!dbIdp) + throw exports.notFoundErr; + return dbIdp.provider; + }, + silo: function (_a) { + var id = _a.silo; + if (!id) + throw exports.notFoundErr; + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(exports.db.silos, id); + var silo = exports.db.silos.find(function (o) { return o.name === id; }); + if (!silo) + throw exports.notFoundErr; + return silo; + }, + sled: function (_a) { + var id = _a.sledId; + if (!id) + throw exports.notFoundErr; + return (0, exports.lookupById)(exports.db.sleds, id); + }, + sshKey: function (_a) { + var id = _a.sshKey; + // we don't have a concept of mock session. assume the user is user1 + var userSshKeys = exports.db.sshKeys.filter(function (key) { return key.silo_user_id === api_mocks_1.user1.id; }); + if ((0, uuid_1.validate)(id)) + return (0, exports.lookupById)(userSshKeys, id); + var sshKey = userSshKeys.find(function (key) { return key.name === id; }); + if (!sshKey) + throw exports.notFoundErr; + return sshKey; + }, +}; +function utilizationForSilo(silo) { + var quotas = exports.db.siloQuotas.find(function (q) { return q.silo_id === silo.id; }); + if (!quotas) + throw (0, util_2.internalError)(); + var provisioned = exports.db.siloProvisioned.find(function (p) { return p.silo_id === silo.id; }); + if (!provisioned) + throw (0, util_2.internalError)(); + return { + allocated: (0, util_1.pick)(quotas, 'cpus', 'storage', 'memory'), + provisioned: (0, util_1.pick)(provisioned, 'cpus', 'storage', 'memory'), + silo_id: silo.id, + silo_name: silo.name, + }; +} +exports.utilizationForSilo = utilizationForSilo; +var initDb = { + disks: __spreadArray([], mock.disks, true), + diskBulkImportState: new Map(), + floatingIps: __spreadArray([], mock.floatingIps, true), + userGroups: __spreadArray([], mock.userGroups, true), + /** Join table for `users` and `userGroups` */ + groupMemberships: __spreadArray([], mock.groupMemberships, true), + images: __spreadArray([], mock.images, true), + externalIps: __spreadArray([], mock.externalIps, true), + instances: __spreadArray([], mock.instances, true), + ipPools: __spreadArray([], mock.ipPools, true), + ipPoolSilos: __spreadArray([], mock.ipPoolSilos, true), + ipPoolRanges: __spreadArray([], mock.ipPoolRanges, true), + networkInterfaces: [mock.networkInterface], + physicalDisks: __spreadArray([], mock.physicalDisks, true), + projects: __spreadArray([], mock.projects, true), + racks: __spreadArray([], mock.racks, true), + roleAssignments: __spreadArray([], mock.roleAssignments, true), + silos: __spreadArray([], mock.silos, true), + siloQuotas: __spreadArray([], mock.siloQuotas, true), + siloProvisioned: __spreadArray([], mock.siloProvisioned, true), + identityProviders: __spreadArray([], mock.identityProviders, true), + sleds: __spreadArray([], mock.sleds, true), + snapshots: __spreadArray([], mock.snapshots, true), + sshKeys: __spreadArray([], mock.sshKeys, true), + users: __spreadArray([], mock.users, true), + vpcFirewallRules: __spreadArray([], mock.defaultFirewallRules, true), + vpcs: [mock.vpc], + vpcSubnets: [mock.vpcSubnet], +}; +exports.db = structuredClone(initDb); +function resetDb() { + exports.db = structuredClone(initDb); +} +exports.resetDb = resetDb; diff --git a/libs/api-mocks/msw/handlers.js b/libs/api-mocks/msw/handlers.js new file mode 100644 index 0000000000..2eb0a18f58 --- /dev/null +++ b/libs/api-mocks/msw/handlers.js @@ -0,0 +1,1107 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handlers = void 0; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +var msw_1 = require("msw"); +var uuid_1 = require("uuid"); +var api_1 = require("@oxide/api"); +var msw_handlers_1 = require("@oxide/gen/msw-handlers"); +var util_1 = require("@oxide/util"); +var metrics_1 = require("../metrics"); +var serial_1 = require("../serial"); +var silo_1 = require("../silo"); +var db_1 = require("./db"); +var util_2 = require("./util"); +// Note the *JSON types. Those represent actual API request and response bodies, +// the snake-cased objects coming straight from the API before the generated +// client camel-cases the keys and parses date fields. Inside the mock API everything +// is *JSON type. +exports.handlers = (0, msw_handlers_1.makeHandlers)({ + ping: function () { return ({ status: 'ok' }); }, + deviceAuthRequest: function () { return 200; }, + deviceAuthConfirm: function (_a) { + var body = _a.body; + return (body.user_code === 'ERRO-RABC' ? 400 : 200); + }, + deviceAccessToken: function () { return 200; }, + loginLocal: function (_a) { + var password = _a.body.password; + return (password === 'bad' ? 401 : 200); + }, + groupList: function (params) { return (0, util_2.paginated)(params.query, db_1.db.userGroups); }, + groupView: function (params) { return (0, db_1.lookupById)(db_1.db.userGroups, params.path.groupId); }, + projectList: function (params) { return (0, util_2.paginated)(params.query, db_1.db.projects); }, + projectCreate: function (_a) { + var body = _a.body; + (0, util_2.errIfExists)(db_1.db.projects, { name: body.name }, 'project'); + var newProject = __assign(__assign({ id: (0, uuid_1.v4)() }, body), (0, util_2.getTimestamps)()); + db_1.db.projects.push(newProject); + return (0, msw_handlers_1.json)(newProject, { status: 201 }); + }, + projectView: function (_a) { + var path = _a.path; + if (path.project.endsWith('error-503')) { + throw util_2.unavailableErr; + } + return db_1.lookup.project(__assign({}, path)); + }, + projectUpdate: function (_a) { + var body = _a.body, path = _a.path; + var project = db_1.lookup.project(__assign({}, path)); + if (body.name) { + // only check for existing name if it's being changed + if (body.name !== project.name) { + (0, util_2.errIfExists)(db_1.db.projects, { name: body.name }); + } + project.name = body.name; + } + project.description = body.description || ''; + return project; + }, + projectDelete: function (_a) { + var path = _a.path; + var project = db_1.lookup.project(__assign({}, path)); + // imitate API logic (TODO: check for every other kind of project child) + if (db_1.db.vpcs.some(function (vpc) { return vpc.project_id === project.id; })) { + throw 'Project to be deleted contains a VPC'; + } + db_1.db.projects = db_1.db.projects.filter(function (p) { return p.id !== project.id; }); + return 204; + }, + diskList: function (_a) { + var query = _a.query; + var project = db_1.lookup.project(query); + var disks = db_1.db.disks.filter(function (d) { return d.project_id === project.id; }); + return (0, util_2.paginated)(query, disks); + }, + diskCreate: function (_a) { + var body = _a.body, query = _a.query; + var project = db_1.lookup.project(query); + (0, util_2.errIfExists)(db_1.db.disks, { name: body.name, project_id: project.id }); + if (body.name === 'disk-create-500') + throw 500; + var name = body.name, description = body.description, size = body.size, disk_source = body.disk_source; + var newDisk = __assign({ id: (0, uuid_1.v4)(), project_id: project.id, state: disk_source.type === 'importing_blocks' + ? { state: 'import_ready' } + : { state: 'creating' }, device_path: '/mnt/disk', name: name, description: description, size: size, + // TODO: for non-blank disk sources, look up image or snapshot by ID and + // pull block size from there + block_size: disk_source.type === 'blank' ? disk_source.block_size : 512 }, (0, util_2.getTimestamps)()); + db_1.db.disks.push(newDisk); + return (0, msw_handlers_1.json)(newDisk, { status: 201 }); + }, + diskView: function (_a) { + var path = _a.path, query = _a.query; + return db_1.lookup.disk(__assign(__assign({}, path), query)); + }, + diskDelete: function (_a) { + var path = _a.path, query = _a.query; + var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); + if (!api_1.diskCan.delete(disk)) { + throw 'Cannot delete disk in state ' + disk.state.state; + } + db_1.db.disks = db_1.db.disks.filter(function (d) { return d.id !== disk.id; }); + return 204; + }, + diskMetricsList: function (_a) { + var path = _a.path, query = _a.query; + db_1.lookup.disk(__assign(__assign({}, path), query)); + var _b = (0, util_2.getStartAndEndTime)(query), startTime = _b.startTime, endTime = _b.endTime; + if (endTime <= startTime) + return { items: [] }; + return { + items: (0, metrics_1.genCumulativeI64Data)(new Array(1000).fill(0).map(function (_x, i) { return Math.floor(Math.tanh(i / 500) * 3000); }), startTime, endTime), + }; + }, + diskBulkWriteImportStart: function (_a) { + var path = _a.path, query = _a.query; + var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); + if (disk.name === 'import-start-500') + throw 500; + if (disk.state.state !== 'import_ready') { + throw 'Can only enter state importing_from_bulk_write from import_ready'; + } + // throw 400 + db_1.db.diskBulkImportState.set(disk.id, { blocks: {} }); + disk.state = { state: 'importing_from_bulk_writes' }; + return 204; + }, + diskBulkWriteImportStop: function (_a) { + var path = _a.path, query = _a.query; + var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); + if (disk.name === 'import-stop-500') + throw 500; + if (disk.state.state !== 'importing_from_bulk_writes') { + throw 'Can only stop import for disk in state importing_from_bulk_write'; + } + db_1.db.diskBulkImportState.delete(disk.id); + disk.state = { state: 'import_ready' }; + return 204; + }, + diskBulkWriteImport: function (_a) { + var path = _a.path, query = _a.query, body = _a.body; + var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); + var diskImport = db_1.db.diskBulkImportState.get(disk.id); + if (!diskImport) + throw db_1.notFoundErr; + // if (Math.random() < 0.01) throw 400 + diskImport.blocks[body.offset] = true; + return 204; + }, + diskFinalizeImport: function (_a) { + var path = _a.path, query = _a.query, body = _a.body; + var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); + if (disk.name === 'disk-finalize-500') + throw 500; + if (disk.state.state !== 'import_ready') { + throw "Cannot finalize disk in state ".concat(disk.state.state, ". Must be import_ready."); + } + // for now, don't check that the file is complete. the API doesn't + disk.state = { state: 'detached' }; + if (body.snapshot_name) { + var newSnapshot = __assign(__assign({ id: (0, uuid_1.v4)(), name: body.snapshot_name, description: 'temporary snapshot for making an image' }, (0, util_2.getTimestamps)()), { state: 'ready', project_id: disk.project_id, disk_id: disk.id, size: disk.size }); + db_1.db.snapshots.push(newSnapshot); + } + return 204; + }, + // floatingIpCreate({ body, query }) { + // const project = lookup.project(query) + // errIfExists(db.floatingIps, { name: body.name }) + // const newFloatingIp: Json = { + // id: uuid(), + // project_id: project.id, + // ip: `${[...Array(4)].map(()=>Math.floor(Math.random()*256)).join('.')}`, + // ...body, + // ...getTimestamps(), + // } + // db.floatingIps.push(newFloatingIp) + // return json(newFloatingIp, { status: 201 }) + // }, + // floatingIpList({ query }) { + // const project = lookup.project(query) + // const ips = db.floatingIps.filter((i) => i.project_id === project.id) + // return paginated(query, ips) + // }, + // floatingIpView({ query, path }) { + // const project = lookup.project(query) + // const ip = db.floatingIps.filter( + // (i) => i.project_id === project.id && i.name === path.floatingIp + // )[0] + // return ip + // }, + // floatingIpDelete({ path, query }) { + // const floatingIp = lookup.floatingIp({ ...path, ...query }) + // db.floatingIps = db.floatingIps.filter((i) => i.id !== floatingIp.id) + // return 204 + // }, + // floatingIpAttach({ body, path, query }) { + // const floatingIp = lookup.floatingIp({ ...path, ...query }) + // const instance = lookup.instance({ instance: body.parent }) + // console.log(instance) + // floatingIp.instance_id = instance.id + // console.log(floatingIp) + // return floatingIp + // }, + // floatingIpDetach({ path, query }) { + // const floatingIp: FloatingIp = lookup.floatingIp({ ...path, ...query }) + // db.floatingIps = db.floatingIps.map((ip : FloatingIp) => (ip.id !== floatingIp.id) ? ip : { ...ip, instance_id: undefined }) + // return 204 + // }, + imageList: function (_a) { + var query = _a.query; + if (query.project) { + var project_1 = db_1.lookup.project(query); + var images_1 = db_1.db.images.filter(function (i) { return i.project_id === project_1.id; }); + return (0, util_2.paginated)(query, images_1); + } + // silo images + var images = db_1.db.images.filter(function (i) { return !i.project_id; }); + return (0, util_2.paginated)(query, images); + }, + imageCreate: function (_a) { + var body = _a.body, query = _a.query; + var project_id = undefined; + if (query.project) { + project_id = db_1.lookup.project(query).id; + } + (0, util_2.errIfExists)(db_1.db.images, { name: body.name, project_id: project_id }); + var size = body.source.type === 'snapshot' + ? db_1.lookup.snapshot({ snapshot: body.source.id }).size + : 100; + var newImage = __assign(__assign({ id: (0, uuid_1.v4)(), project_id: project_id, size: size, block_size: 512 }, body), (0, util_2.getTimestamps)()); + db_1.db.images.push(newImage); + return (0, msw_handlers_1.json)(newImage, { status: 201 }); + }, + imageView: function (_a) { + var path = _a.path, query = _a.query; + return db_1.lookup.image(__assign(__assign({}, path), query)); + }, + imageDelete: function (_a) { + var path = _a.path, query = _a.query, cookies = _a.cookies; + // if it's a silo image, you need silo write to delete it + if (!query.project) { + (0, util_2.requireRole)(cookies, 'silo', silo_1.defaultSilo.id, 'collaborator'); + } + var image = db_1.lookup.image(__assign(__assign({}, path), query)); + db_1.db.images = db_1.db.images.filter(function (i) { return i.id !== image.id; }); + return 204; + }, + imagePromote: function (_a) { + var path = _a.path, query = _a.query; + var image = db_1.lookup.image(__assign(__assign({}, path), query)); + delete image.project_id; + return (0, msw_handlers_1.json)(image, { status: 202 }); + }, + imageDemote: function (_a) { + var path = _a.path, query = _a.query; + var image = db_1.lookup.image(__assign(__assign({}, path), query)); + var project = db_1.lookup.project(__assign(__assign({}, path), query)); + image.project_id = project.id; + return (0, msw_handlers_1.json)(image, { status: 202 }); + }, + instanceList: function (_a) { + var query = _a.query; + var project = db_1.lookup.project(query); + var instances = db_1.db.instances.filter(function (i) { return i.project_id === project.id; }); + return (0, util_2.paginated)(query, instances); + }, + instanceCreate: function (_a) { + var _b, _c, _d; + var body = _a.body, query = _a.query; + return __awaiter(this, void 0, void 0, function () { + var project, instanceId, _i, _e, diskParams, _f, _g, diskParams, size, name_1, description, disk_source, newDisk, disk, anyVpc, anySubnet, newInstance; + return __generator(this, function (_h) { + project = db_1.lookup.project(query); + if (body.name === 'no-default-pool') { + throw (0, db_1.notFoundErr)('default IP pool for current silo'); + } + (0, util_2.errIfExists)(db_1.db.instances, { name: body.name, project_id: project.id }, 'instance'); + instanceId = (0, uuid_1.v4)(); + // TODO: These values should ultimately be represented in the schema and + // checked with the generated schema validation code. + if (body.memory > api_1.INSTANCE_MAX_RAM_GiB * util_1.GiB) { + throw "Memory must be less than ".concat(api_1.INSTANCE_MAX_RAM_GiB, " GiB"); + } + if (body.memory < api_1.INSTANCE_MIN_RAM_GiB * util_1.GiB) { + throw "Memory must be at least ".concat(api_1.INSTANCE_MIN_RAM_GiB, " GiB"); + } + if (body.ncpus > api_1.INSTANCE_MAX_CPU) { + throw "vCPUs must be less than ".concat(api_1.INSTANCE_MAX_CPU); + } + if (body.ncpus < 1) { + throw "Must have at least 1 vCPU"; + } + /** + * Eagerly check for disk errors. Execution will stop early and prevent orphaned disks from + * being created if there's a failure. In omicron this is done automatically via an undo on the saga. + */ + for (_i = 0, _e = body.disks || []; _i < _e.length; _i++) { + diskParams = _e[_i]; + if (diskParams.type === 'create') { + (0, util_2.errIfExists)(db_1.db.disks, { name: diskParams.name, project_id: project.id }, 'disk'); + (0, util_2.errIfInvalidDiskSize)(diskParams); + } + else { + db_1.lookup.disk(__assign(__assign({}, query), { disk: diskParams.name })); + } + } + /** + * Eagerly check for nic lookup failures. Execution will stop early and prevent orphaned nics from + * being created if there's a failure. In omicron this is done automatically via an undo on the saga. + */ + if (((_b = body.network_interfaces) === null || _b === void 0 ? void 0 : _b.type) === 'create') { + if (body.network_interfaces.params.length > api_1.MAX_NICS_PER_INSTANCE) { + throw "Cannot create more than ".concat(api_1.MAX_NICS_PER_INSTANCE, " nics per instance"); + } + body.network_interfaces.params.forEach(function (_a) { + var vpc_name = _a.vpc_name, subnet_name = _a.subnet_name; + db_1.lookup.vpc(__assign(__assign({}, query), { vpc: vpc_name })); + db_1.lookup.vpcSubnet(__assign(__assign({}, query), { vpc: vpc_name, subnet: subnet_name })); + }); + } + for (_f = 0, _g = body.disks || []; _f < _g.length; _f++) { + diskParams = _g[_f]; + if (diskParams.type === 'create') { + size = diskParams.size, name_1 = diskParams.name, description = diskParams.description, disk_source = diskParams.disk_source; + newDisk = __assign({ id: (0, uuid_1.v4)(), name: name_1, description: description, size: size, project_id: project.id, state: { state: 'attached', instance: instanceId }, device_path: '/mnt/disk', block_size: disk_source.type === 'blank' ? disk_source.block_size : 4096 }, (0, util_2.getTimestamps)()); + db_1.db.disks.push(newDisk); + } + else { + disk = db_1.lookup.disk(__assign(__assign({}, query), { disk: diskParams.name })); + disk.state = { state: 'attached', instance: instanceId }; + } + } + anyVpc = db_1.db.vpcs.find(function (v) { return v.project_id === project.id; }); + anySubnet = db_1.db.vpcSubnets.find(function (s) { return s.vpc_id === (anyVpc === null || anyVpc === void 0 ? void 0 : anyVpc.id); }); + if (((_c = body.network_interfaces) === null || _c === void 0 ? void 0 : _c.type) === 'default' && anyVpc && anySubnet) { + db_1.db.networkInterfaces.push(__assign({ id: (0, uuid_1.v4)(), description: 'The default network interface', instance_id: instanceId, primary: true, mac: '00:00:00:00:00:00', ip: '127.0.0.1', name: 'default', vpc_id: anyVpc.id, subnet_id: anySubnet.id }, (0, util_2.getTimestamps)())); + } + else if (((_d = body.network_interfaces) === null || _d === void 0 ? void 0 : _d.type) === 'create') { + body.network_interfaces.params.forEach(function (_a, i) { + var name = _a.name, description = _a.description, ip = _a.ip, subnet_name = _a.subnet_name, vpc_name = _a.vpc_name; + db_1.db.networkInterfaces.push(__assign({ id: (0, uuid_1.v4)(), name: name, description: description, instance_id: instanceId, primary: i === 0 ? true : false, mac: '00:00:00:00:00:00', ip: ip || '127.0.0.1', vpc_id: db_1.lookup.vpc(__assign(__assign({}, query), { vpc: vpc_name })).id, subnet_id: db_1.lookup.vpcSubnet(__assign(__assign({}, query), { vpc: vpc_name, subnet: subnet_name })) + .id }, (0, util_2.getTimestamps)())); + }); + } + newInstance = __assign(__assign(__assign({ id: instanceId, project_id: project.id }, (0, util_1.pick)(body, 'name', 'description', 'hostname', 'memory', 'ncpus')), (0, util_2.getTimestamps)()), { run_state: 'running', time_run_state_updated: new Date().toISOString() }); + db_1.db.instances.push(newInstance); + return [2 /*return*/, (0, msw_handlers_1.json)(newInstance, { status: 201 })]; + }); + }); + }, + instanceView: function (_a) { + var path = _a.path, query = _a.query; + return db_1.lookup.instance(__assign(__assign({}, path), query)); + }, + instanceDelete: function (_a) { + var path = _a.path, query = _a.query; + var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); + db_1.db.instances = db_1.db.instances.filter(function (i) { return i.id !== instance.id; }); + return 204; + }, + instanceDiskList: function (_a) { + var path = _a.path, query = _a.query; + var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); + // TODO: Should disk instance state be `instance_id` instead of `instance`? + var disks = db_1.db.disks.filter(function (d) { return 'instance' in d.state && d.state.instance === instance.id; }); + return (0, util_2.paginated)(query, disks); + }, + instanceDiskAttach: function (_a) { + var body = _a.body, path = _a.path, projectParams = _a.query; + var instance = db_1.lookup.instance(__assign(__assign({}, path), projectParams)); + if (instance.run_state !== 'stopped') { + throw 'Cannot attach disk to instance that is not stopped'; + } + var disk = db_1.lookup.disk(__assign(__assign({}, projectParams), { disk: body.disk })); + disk.state = { + state: 'attached', + instance: instance.id, + }; + return disk; + }, + instanceDiskDetach: function (_a) { + var body = _a.body, path = _a.path, projectParams = _a.query; + var instance = db_1.lookup.instance(__assign(__assign({}, path), projectParams)); + if (instance.run_state !== 'stopped') { + throw 'Cannot detach disk from instance that is not stopped'; + } + var disk = db_1.lookup.disk(__assign(__assign({}, projectParams), { disk: body.disk })); + disk.state = { state: 'detached' }; + return disk; + }, + instanceExternalIpList: function (_a) { + var path = _a.path, query = _a.query; + var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); + var externalIps = db_1.db.externalIps + .filter(function (eip) { return eip.instance_id === instance.id; }) + .map(function (eip) { return eip.external_ip; }); + // endpoint is not paginated. or rather, it's fake paginated + return { items: externalIps }; + }, + instanceNetworkInterfaceList: function (_a) { + var query = _a.query; + var instance = db_1.lookup.instance(query); + var nics = db_1.db.networkInterfaces.filter(function (n) { return n.instance_id === instance.id; }); + return (0, util_2.paginated)(query, nics); + }, + instanceNetworkInterfaceCreate: function (_a) { + var body = _a.body, query = _a.query; + var instance = db_1.lookup.instance(query); + var nicsForInstance = db_1.db.networkInterfaces.filter(function (n) { return n.instance_id === instance.id; }); + (0, util_2.errIfExists)(nicsForInstance, { name: body.name }); + var name = body.name, description = body.description, subnet_name = body.subnet_name, vpc_name = body.vpc_name, ip = body.ip; + var vpc = db_1.lookup.vpc(__assign(__assign({}, query), { vpc: vpc_name })); + var subnet = db_1.lookup.vpcSubnet(__assign(__assign({}, query), { vpc: vpc_name, subnet: subnet_name })); + var newNic = __assign({ id: (0, uuid_1.v4)(), + // matches API logic: https://github.com/oxidecomputer/omicron/blob/ae22982/nexus/src/db/queries/network_interface.rs#L982-L1015 + primary: nicsForInstance.length === 0, instance_id: instance.id, name: name, description: description, ip: ip || '123.45.68.8', vpc_id: vpc.id, subnet_id: subnet.id, mac: '' }, (0, util_2.getTimestamps)()); + db_1.db.networkInterfaces.push(newNic); + return newNic; + }, + instanceNetworkInterfaceView: function (_a) { + var path = _a.path, query = _a.query; + return db_1.lookup.networkInterface(__assign(__assign({}, path), query)); + }, + instanceNetworkInterfaceUpdate: function (_a) { + var body = _a.body, path = _a.path, query = _a.query; + var nic = db_1.lookup.networkInterface(__assign(__assign({}, path), query)); + if (body.name) { + nic.name = body.name; + } + if (typeof body.description === 'string') { + nic.description = body.description; + } + if (typeof body.primary === 'boolean' && body.primary !== nic.primary) { + if (nic.primary) { + throw 'Cannot remove the primary interface'; + } + db_1.db.networkInterfaces + .filter(function (n) { return n.instance_id === nic.instance_id; }) + .forEach(function (n) { + n.primary = false; + }); + nic.primary = !!body.primary; + } + return nic; + }, + instanceNetworkInterfaceDelete: function (_a) { + var path = _a.path, query = _a.query; + var nic = db_1.lookup.networkInterface(__assign(__assign({}, path), query)); + db_1.db.networkInterfaces = db_1.db.networkInterfaces.filter(function (n) { return n.id !== nic.id; }); + return 204; + }, + instanceReboot: function (_a) { + var path = _a.path, query = _a.query; + var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); + instance.run_state = 'rebooting'; + setTimeout(function () { + instance.run_state = 'running'; + }, 3000); + return (0, msw_handlers_1.json)(instance, { status: 202 }); + }, + instanceSerialConsole: function (_params) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, (0, msw_1.delay)(3000)]; + case 1: + _a.sent(); + return [2 /*return*/, (0, msw_handlers_1.json)(serial_1.serial)]; + } + }); + }); + }, + instanceStart: function (_a) { + var path = _a.path, query = _a.query; + var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); + instance.run_state = 'running'; + return (0, msw_handlers_1.json)(instance, { status: 202 }); + }, + instanceStop: function (_a) { + var path = _a.path, query = _a.query; + var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); + instance.run_state = 'stopped'; + return (0, msw_handlers_1.json)(instance, { status: 202 }); + }, + ipPoolList: function (_a) { + var query = _a.query; + return (0, util_2.paginated)(query, db_1.db.ipPools); + }, + siloIpPoolList: function (_a) { + var path = _a.path, query = _a.query; + var pools = db_1.lookup.siloIpPools(path); + return (0, util_2.paginated)(query, pools); + }, + projectIpPoolList: function (_a) { + var query = _a.query; + var pools = db_1.lookup.siloIpPools({ silo: silo_1.defaultSilo.id }); + return (0, util_2.paginated)(query, pools); + }, + projectIpPoolView: function (_a) { + var path = _a.path; + // this will 404 if it doesn't exist at all... + var pool = db_1.lookup.ipPool(path); + // but we also want to 404 if it exists but isn't in the silo + var link = db_1.db.ipPoolSilos.find(function (link) { return link.ip_pool_id === pool.id && link.silo_id === silo_1.defaultSilo.id; }); + if (!link) + throw (0, db_1.notFoundErr)(); + return __assign(__assign({}, pool), { is_default: link.is_default }); + }, + // TODO: require admin permissions for system IP pool endpoints + ipPoolView: function (_a) { + var path = _a.path; + return db_1.lookup.ipPool(path); + }, + ipPoolSiloList: function (_a) { + // TODO: paginated wants an id field, but this is a join table, so it has a + // composite pk + // return paginated(query, db.ipPoolResources) + var path = _a.path /*query*/; + var pool = db_1.lookup.ipPool(path); + var assocs = db_1.db.ipPoolSilos.filter(function (ipr) { return ipr.ip_pool_id === pool.id; }); + return { items: assocs }; + }, + ipPoolSiloLink: function (_a) { + var path = _a.path, body = _a.body; + var pool = db_1.lookup.ipPool(path); + var silo_id = db_1.lookup.silo({ silo: body.silo }).id; + var assoc = { + ip_pool_id: pool.id, + silo_id: silo_id, + is_default: body.is_default, + }; + var alreadyThere = db_1.db.ipPoolSilos.find(function (ips) { return ips.ip_pool_id === pool.id && ips.silo_id === silo_id; }); + // TODO: this matches current API logic but makes no sense because is_default + // could be different. Need to fix that. Should 400 or 409 on conflict. + if (!alreadyThere) + db_1.db.ipPoolSilos.push(assoc); + return assoc; + }, + ipPoolSiloUnlink: function (_a) { + var path = _a.path; + var pool = db_1.lookup.ipPool(path); + var silo = db_1.lookup.silo(path); + // ignore is_default when deleting, it's not part of the pk + db_1.db.ipPoolSilos = db_1.db.ipPoolSilos.filter(function (ips) { return !(ips.ip_pool_id === pool.id && ips.silo_id === silo.id); }); + return 204; + }, + ipPoolSiloUpdate: function (_a) { + var path = _a.path, body = _a.body; + var ipPoolSilo = db_1.lookup.ipPoolSiloLink(path); + // if we're setting default, we need to set is_default false on the existing default + if (body.is_default) { + var silo_2 = db_1.lookup.silo(path); + var existingDefault = db_1.db.ipPoolSilos.find(function (ips) { return ips.silo_id === silo_2.id && ips.is_default; }); + if (existingDefault) { + existingDefault.is_default = false; + } + } + ipPoolSilo.is_default = body.is_default; + return ipPoolSilo; + }, + ipPoolRangeList: function (_a) { + var path = _a.path, query = _a.query; + var pool = db_1.lookup.ipPool(path); + var ranges = db_1.db.ipPoolRanges.filter(function (r) { return r.ip_pool_id === pool.id; }); + return (0, util_2.paginated)(query, ranges); + }, + ipPoolRangeAdd: function (_a) { + var path = _a.path, body = _a.body; + var pool = db_1.lookup.ipPool(path); + var newRange = { + id: (0, uuid_1.v4)(), + ip_pool_id: pool.id, + range: body, + time_created: new Date().toISOString(), + }; + // TODO: validate that it doesn't overlap with existing ranges + db_1.db.ipPoolRanges.push(newRange); + return (0, msw_handlers_1.json)(newRange, { status: 201 }); + }, + ipPoolRangeRemove: function (_a) { + var path = _a.path, body = _a.body; + var pool = db_1.lookup.ipPool(path); + var idsToDelete = db_1.db.ipPoolRanges + .filter(function (r) { + return r.ip_pool_id === pool.id && + r.range.first === body.first && + r.range.last === body.last; + }) + .map(function (r) { return r.id; }); + // if nothing in the DB matches, 404 + if (idsToDelete.length === 0) + throw (0, db_1.notFoundErr)(); + db_1.db.ipPoolRanges = db_1.db.ipPoolRanges.filter(function (r) { return !idsToDelete.includes(r.id); }); + return 204; + }, + ipPoolCreate: function (_a) { + var body = _a.body; + (0, util_2.errIfExists)(db_1.db.ipPools, { name: body.name }, 'IP pool'); + var newPool = __assign(__assign({ id: (0, uuid_1.v4)() }, body), (0, util_2.getTimestamps)()); + db_1.db.ipPools.push(newPool); + return (0, msw_handlers_1.json)(newPool, { status: 201 }); + }, + ipPoolDelete: function (_a) { + var path = _a.path; + var pool = db_1.lookup.ipPool(path); + if (db_1.db.ipPoolRanges.some(function (r) { return r.ip_pool_id === pool.id; })) { + throw 'IP pool cannot be deleted while it contains IP ranges'; + } + // delete pools and silo links + db_1.db.ipPools = db_1.db.ipPools.filter(function (p) { return p.id !== pool.id; }); + db_1.db.ipPoolSilos = db_1.db.ipPoolSilos.filter(function (s) { return s.ip_pool_id !== pool.id; }); + return 204; + }, + ipPoolUpdate: function (_a) { + var path = _a.path, body = _a.body; + var pool = db_1.lookup.ipPool(path); + if (body.name) { + // only check for existing name if it's being changed + if (body.name !== pool.name) { + (0, util_2.errIfExists)(db_1.db.ipPools, { name: body.name }); + } + pool.name = body.name; + } + pool.description = body.description || ''; + return pool; + }, + projectPolicyView: function (_a) { + var path = _a.path; + var project = db_1.lookup.project(path); + var role_assignments = db_1.db.roleAssignments + .filter(function (r) { return r.resource_type === 'project' && r.resource_id === project.id; }) + .map(function (r) { return (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'); }); + return { role_assignments: role_assignments }; + }, + projectPolicyUpdate: function (_a) { + var body = _a.body, path = _a.path; + var project = db_1.lookup.project(path); + var newAssignments = body.role_assignments.map(function (r) { return (__assign({ resource_type: 'project', resource_id: project.id }, (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'))); }); + var unrelatedAssignments = db_1.db.roleAssignments.filter(function (r) { return !(r.resource_type === 'project' && r.resource_id === project.id); }); + db_1.db.roleAssignments = __spreadArray(__spreadArray([], unrelatedAssignments, true), newAssignments, true); + // TODO: Is this the right thing to return? + return body; + }, + snapshotList: function (params) { + var project = db_1.lookup.project(params.query); + var snapshots = db_1.db.snapshots.filter(function (i) { return i.project_id === project.id; }); + return (0, util_2.paginated)(params.query, snapshots); + }, + snapshotCreate: function (_a) { + var body = _a.body, query = _a.query; + var project = db_1.lookup.project(query); + if (body.disk === 'disk-snapshot-error') { + throw 'Cannot snapshot disk'; + } + (0, util_2.errIfExists)(db_1.db.snapshots, { name: body.name }); + var disk = db_1.lookup.disk(__assign(__assign({}, query), { disk: body.disk })); + if (!api_1.diskCan.snapshot(disk)) { + throw 'Cannot snapshot disk in state ' + disk.state.state; + } + var newSnapshot = __assign(__assign(__assign({ id: (0, uuid_1.v4)() }, body), (0, util_2.getTimestamps)()), { state: 'ready', project_id: project.id, disk_id: disk.id, size: disk.size }); + db_1.db.snapshots.push(newSnapshot); + return (0, msw_handlers_1.json)(newSnapshot, { status: 201 }); + }, + snapshotView: function (_a) { + var path = _a.path, query = _a.query; + return db_1.lookup.snapshot(__assign(__assign({}, path), query)); + }, + snapshotDelete: function (_a) { + var path = _a.path, query = _a.query; + if (path.snapshot === 'delete-500') + return 500; + var snapshot = db_1.lookup.snapshot(__assign(__assign({}, path), query)); + db_1.db.snapshots = db_1.db.snapshots.filter(function (s) { return s.id !== snapshot.id; }); + return 204; + }, + utilizationView: function () { + var _a = (0, db_1.utilizationForSilo)(silo_1.defaultSilo), capacity = _a.allocated, provisioned = _a.provisioned; + return { capacity: capacity, provisioned: provisioned }; + }, + siloUtilizationView: function (_a) { + var path = _a.path; + var silo = db_1.lookup.silo(path); + return (0, db_1.utilizationForSilo)(silo); + }, + siloUtilizationList: function (_a) { + var query = _a.query; + var _b = (0, util_2.paginated)(query, db_1.db.silos), silos = _b.items, nextPage = _b.nextPage; + return { + items: silos.map(db_1.utilizationForSilo), + nextPage: nextPage, + }; + }, + vpcList: function (_a) { + var query = _a.query; + var project = db_1.lookup.project(query); + var vpcs = db_1.db.vpcs.filter(function (v) { return v.project_id === project.id; }); + return (0, util_2.paginated)(query, vpcs); + }, + vpcCreate: function (_a) { + var body = _a.body, query = _a.query; + var project = db_1.lookup.project(query); + (0, util_2.errIfExists)(db_1.db.vpcs, { name: body.name }); + var newVpc = __assign(__assign(__assign({ id: (0, uuid_1.v4)(), project_id: project.id, system_router_id: (0, uuid_1.v4)() }, body), { + // API is supposed to generate one if none provided. close enough + ipv6_prefix: body.ipv6_prefix || 'fd2d:4569:88b2::/64' }), (0, util_2.getTimestamps)()); + db_1.db.vpcs.push(newVpc); + // Also create a default subnet + var newSubnet = __assign({ id: (0, uuid_1.v4)(), name: 'default', vpc_id: newVpc.id, ipv6_block: 'fd2d:4569:88b1::/64', description: '', ipv4_block: '' }, (0, util_2.getTimestamps)()); + db_1.db.vpcSubnets.push(newSubnet); + return (0, msw_handlers_1.json)(newVpc, { status: 201 }); + }, + vpcView: function (_a) { + var path = _a.path, query = _a.query; + return db_1.lookup.vpc(__assign(__assign({}, path), query)); + }, + vpcUpdate: function (_a) { + var body = _a.body, path = _a.path, query = _a.query; + var vpc = db_1.lookup.vpc(__assign(__assign({}, path), query)); + if (body.name) { + vpc.name = body.name; + } + if (typeof body.description === 'string') { + vpc.description = body.description; + } + if (body.dns_name) { + vpc.dns_name = body.dns_name; + } + return vpc; + }, + vpcDelete: function (_a) { + var path = _a.path, query = _a.query; + var vpc = db_1.lookup.vpc(__assign(__assign({}, path), query)); + db_1.db.vpcs = db_1.db.vpcs.filter(function (v) { return v.id !== vpc.id; }); + db_1.db.vpcSubnets = db_1.db.vpcSubnets.filter(function (s) { return s.vpc_id !== vpc.id; }); + db_1.db.vpcFirewallRules = db_1.db.vpcFirewallRules.filter(function (r) { return r.vpc_id !== vpc.id; }); + return 204; + }, + vpcFirewallRulesView: function (_a) { + var query = _a.query; + var vpc = db_1.lookup.vpc(query); + var rules = db_1.db.vpcFirewallRules.filter(function (r) { return r.vpc_id === vpc.id; }); + return { rules: (0, util_1.sortBy)(rules, function (r) { return r.name; }) }; + }, + vpcFirewallRulesUpdate: function (_a) { + var body = _a.body, query = _a.query; + var vpc = db_1.lookup.vpc(query); + var rules = body.rules.map(function (rule) { return (__assign(__assign({ vpc_id: vpc.id, id: (0, uuid_1.v4)() }, rule), (0, util_2.getTimestamps)())); }); + // replace existing rules for this VPC with the new ones + db_1.db.vpcFirewallRules = __spreadArray(__spreadArray([], db_1.db.vpcFirewallRules.filter(function (r) { return r.vpc_id !== vpc.id; }), true), rules, true); + return { rules: (0, util_1.sortBy)(rules, function (r) { return r.name; }) }; + }, + vpcSubnetList: function (_a) { + var query = _a.query; + var vpc = db_1.lookup.vpc(query); + var subnets = db_1.db.vpcSubnets.filter(function (s) { return s.vpc_id === vpc.id; }); + return (0, util_2.paginated)(query, subnets); + }, + vpcSubnetCreate: function (_a) { + var body = _a.body, query = _a.query; + var vpc = db_1.lookup.vpc(query); + (0, util_2.errIfExists)(db_1.db.vpcSubnets, { vpc_id: vpc.id, name: body.name }); + // TODO: Create a route for the subnet in the default router + var newSubnet = __assign(__assign(__assign({ id: (0, uuid_1.v4)(), vpc_id: vpc.id }, body), { + // required in subnet create but not in update, so we need a fallback. + // API says "A random `/64` block will be assigned if one is not + // provided." Our fallback is not random, but it should be good enough. + ipv6_block: body.ipv6_block || 'fd2d:4569:88b1::/64' }), (0, util_2.getTimestamps)()); + db_1.db.vpcSubnets.push(newSubnet); + return (0, msw_handlers_1.json)(newSubnet, { status: 201 }); + }, + vpcSubnetView: function (_a) { + var path = _a.path, query = _a.query; + return db_1.lookup.vpcSubnet(__assign(__assign({}, path), query)); + }, + vpcSubnetUpdate: function (_a) { + var body = _a.body, path = _a.path, query = _a.query; + var subnet = db_1.lookup.vpcSubnet(__assign(__assign({}, path), query)); + if (body.name) { + subnet.name = body.name; + } + if (typeof body.description === 'string') { + subnet.description = body.description; + } + return subnet; + }, + vpcSubnetDelete: function (_a) { + var path = _a.path, query = _a.query; + var subnet = db_1.lookup.vpcSubnet(__assign(__assign({}, path), query)); + db_1.db.vpcSubnets = db_1.db.vpcSubnets.filter(function (s) { return s.id !== subnet.id; }); + return 204; + }, + vpcSubnetListNetworkInterfaces: function (_a) { + var path = _a.path, query = _a.query; + var subnet = db_1.lookup.vpcSubnet(__assign(__assign({}, path), query)); + var nics = db_1.db.networkInterfaces.filter(function (n) { return n.subnet_id === subnet.id; }); + return (0, util_2.paginated)(query, nics); + }, + sledPhysicalDiskList: function (_a) { + var path = _a.path, query = _a.query, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + var sled = db_1.lookup.sled(path); + var disks = db_1.db.physicalDisks.filter(function (n) { return n.sled_id === sled.id; }); + return (0, util_2.paginated)(query, disks); + }, + physicalDiskList: function (_a) { + var query = _a.query, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + return (0, util_2.paginated)(query, db_1.db.physicalDisks); + }, + policyView: function () { + // assume we're in the default silo + var siloId = silo_1.defaultSilo.id; + var role_assignments = db_1.db.roleAssignments + .filter(function (r) { return r.resource_type === 'silo' && r.resource_id === siloId; }) + .map(function (r) { return (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'); }); + return { role_assignments: role_assignments }; + }, + policyUpdate: function (_a) { + var body = _a.body; + var siloId = silo_1.defaultSilo.id; + var newAssignments = body.role_assignments.map(function (r) { return (__assign({ resource_type: 'silo', resource_id: siloId }, (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'))); }); + var unrelatedAssignments = db_1.db.roleAssignments.filter(function (r) { return !(r.resource_type === 'silo' && r.resource_id === siloId); }); + db_1.db.roleAssignments = __spreadArray(__spreadArray([], unrelatedAssignments, true), newAssignments, true); + return body; + }, + rackList: function (_a) { + var query = _a.query, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + return (0, util_2.paginated)(query, db_1.db.racks); + }, + currentUserView: function (_a) { + var cookies = _a.cookies; + return __assign(__assign({}, (0, util_2.currentUser)(cookies)), { silo_name: silo_1.defaultSilo.name }); + }, + currentUserGroups: function (_a) { + var cookies = _a.cookies; + var user = (0, util_2.currentUser)(cookies); + var memberships = db_1.db.groupMemberships.filter(function (gm) { return gm.userId === user.id; }); + var groupIds = new Set(memberships.map(function (gm) { return gm.groupId; })); + var groups = db_1.db.userGroups.filter(function (g) { return groupIds.has(g.id); }); + return { items: groups }; + }, + currentUserSshKeyList: function (_a) { + var query = _a.query, cookies = _a.cookies; + var user = (0, util_2.currentUser)(cookies); + var keys = db_1.db.sshKeys.filter(function (k) { return k.silo_user_id === user.id; }); + return (0, util_2.paginated)(query, keys); + }, + currentUserSshKeyCreate: function (_a) { + var body = _a.body, cookies = _a.cookies; + var user = (0, util_2.currentUser)(cookies); + (0, util_2.errIfExists)(db_1.db.sshKeys, { silo_user_id: user.id, name: body.name }); + var newSshKey = __assign(__assign({ id: (0, uuid_1.v4)(), silo_user_id: user.id }, body), (0, util_2.getTimestamps)()); + db_1.db.sshKeys.push(newSshKey); + return (0, msw_handlers_1.json)(newSshKey, { status: 201 }); + }, + currentUserSshKeyView: function (_a) { + var path = _a.path; + return db_1.lookup.sshKey(path); + }, + currentUserSshKeyDelete: function (_a) { + var path = _a.path; + var sshKey = db_1.lookup.sshKey(path); + db_1.db.sshKeys = db_1.db.sshKeys.filter(function (i) { return i.id !== sshKey.id; }); + return 204; + }, + sledView: function (_a) { + var path = _a.path, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + return db_1.lookup.sled(path); + }, + sledList: function (_a) { + var query = _a.query, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + return (0, util_2.paginated)(query, db_1.db.sleds); + }, + sledInstanceList: function (_a) { + var query = _a.query, path = _a.path, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + var sled = (0, db_1.lookupById)(db_1.db.sleds, path.sledId); + return (0, util_2.paginated)(query, db_1.db.instances.map(function (i) { + var project = (0, db_1.lookupById)(db_1.db.projects, i.project_id); + return __assign(__assign({}, (0, util_1.pick)(i, 'id', 'name', 'time_created', 'time_modified', 'memory', 'ncpus')), { state: 'running', active_sled_id: sled.id, project_name: project.name, silo_name: silo_1.defaultSilo.name }); + })); + }, + siloList: function (_a) { + var query = _a.query, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + return (0, util_2.paginated)(query, db_1.db.silos); + }, + siloCreate: function (_a) { + var body = _a.body, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + (0, util_2.errIfExists)(db_1.db.silos, { name: body.name }); + var newSilo = __assign(__assign(__assign({ id: (0, uuid_1.v4)() }, (0, util_2.getTimestamps)()), body), { mapped_fleet_roles: body.mapped_fleet_roles || {} }); + db_1.db.silos.push(newSilo); + return (0, msw_handlers_1.json)(newSilo, { status: 201 }); + }, + siloView: function (_a) { + var path = _a.path, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + return db_1.lookup.silo(path); + }, + siloDelete: function (_a) { + var path = _a.path, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + var silo = db_1.lookup.silo(path); + db_1.db.silos = db_1.db.silos.filter(function (i) { return i.id !== silo.id; }); + db_1.db.ipPoolSilos = db_1.db.ipPoolSilos.filter(function (i) { return i.silo_id !== silo.id; }); + return 204; + }, + siloIdentityProviderList: function (_a) { + var query = _a.query, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + var silo = db_1.lookup.silo(query); + var idps = db_1.db.identityProviders.filter(function (_a) { + var siloId = _a.siloId; + return siloId === silo.id; + }).map(silo_1.toIdp); + return { items: idps }; + }, + samlIdentityProviderCreate: function (_a) { + var _b; + var query = _a.query, body = _a.body, cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + var silo = db_1.lookup.silo(query); + // this is a bit silly, but errIfExists doesn't handle nested keys like + // provider.name, so to do the check we make a flatter object + (0, util_2.errIfExists)(db_1.db.identityProviders.map(function (_a) { + var siloId = _a.siloId, provider = _a.provider; + return ({ siloId: siloId, name: provider.name }); + }), { siloId: silo.id, name: body.name }); + // we just decode to string and store that, which is probably fine for local + // dev, but note that the API decodes to bytes and passes that to + // https://docs.rs/openssl/latest/openssl/x509/struct.X509.html#method.from_der + // and that will error if can't be parsed that way + var public_cert = (_b = body.signing_keypair) === null || _b === void 0 ? void 0 : _b.public_cert; + public_cert = public_cert ? atob(public_cert) : undefined; + // we ignore the private key because it's not returned in the get response, + // so you'll never see it again. But worth noting that in the real thing + // it is parsed with this + // https://docs.rs/openssl/latest/openssl/rsa/struct.Rsa.html#method.private_key_from_der + var provider = __assign(__assign(__assign({ id: (0, uuid_1.v4)() }, (0, util_1.pick)(body, 'name', 'acs_url', 'description', 'idp_entity_id', 'slo_url', 'sp_client_id', 'technical_contact_email')), { public_cert: public_cert }), (0, util_2.getTimestamps)()); + db_1.db.identityProviders.push({ type: 'saml', siloId: silo.id, provider: provider }); + return provider; + }, + samlIdentityProviderView: function (_a) { + var path = _a.path, query = _a.query; + return db_1.lookup.samlIdp(__assign(__assign({}, path), query)); + }, + userList: function (_a) { + var query = _a.query; + // query.group is validated by generated code to be a UUID if present + if (query.group) { + var group_1 = (0, db_1.lookupById)(db_1.db.userGroups, query.group); // 404 if doesn't exist + var memberships = db_1.db.groupMemberships.filter(function (gm) { return gm.groupId === group_1.id; }); + var userIds_1 = new Set(memberships.map(function (gm) { return gm.userId; })); + var users = db_1.db.users.filter(function (u) { return userIds_1.has(u.id); }); + return (0, util_2.paginated)(query, users); + } + return (0, util_2.paginated)(query, db_1.db.users); + }, + systemPolicyView: function (_a) { + var cookies = _a.cookies; + (0, util_2.requireFleetViewer)(cookies); + var role_assignments = db_1.db.roleAssignments + .filter(function (r) { return r.resource_type === 'fleet' && r.resource_id === api_1.FLEET_ID; }) + .map(function (r) { return (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'); }); + return { role_assignments: role_assignments }; + }, + systemMetric: function (params) { + (0, util_2.requireFleetViewer)(params.cookies); + return (0, util_2.handleMetrics)(params); + }, + siloMetric: util_2.handleMetrics, + // Misc endpoints we're not using yet in the console + certificateCreate: util_2.NotImplemented, + certificateDelete: util_2.NotImplemented, + certificateList: util_2.NotImplemented, + certificateView: util_2.NotImplemented, + floatingIpAttach: util_2.NotImplemented, + floatingIpCreate: util_2.NotImplemented, + floatingIpDelete: util_2.NotImplemented, + floatingIpDetach: util_2.NotImplemented, + floatingIpList: util_2.NotImplemented, + floatingIpView: util_2.NotImplemented, + instanceEphemeralIpDetach: util_2.NotImplemented, + instanceEphemeralIpAttach: util_2.NotImplemented, + instanceMigrate: util_2.NotImplemented, + instanceSerialConsoleStream: util_2.NotImplemented, + instanceSshPublicKeyList: util_2.NotImplemented, + ipPoolServiceRangeAdd: util_2.NotImplemented, + ipPoolServiceRangeList: util_2.NotImplemented, + ipPoolServiceRangeRemove: util_2.NotImplemented, + ipPoolServiceView: util_2.NotImplemented, + localIdpUserCreate: util_2.NotImplemented, + localIdpUserDelete: util_2.NotImplemented, + localIdpUserSetPassword: util_2.NotImplemented, + loginSaml: util_2.NotImplemented, + logout: util_2.NotImplemented, + networkingAddressLotBlockList: util_2.NotImplemented, + networkingAddressLotCreate: util_2.NotImplemented, + networkingAddressLotDelete: util_2.NotImplemented, + networkingAddressLotList: util_2.NotImplemented, + networkingBfdDisable: util_2.NotImplemented, + networkingBfdEnable: util_2.NotImplemented, + networkingBfdStatus: util_2.NotImplemented, + networkingBgpAnnounceSetCreate: util_2.NotImplemented, + networkingBgpAnnounceSetDelete: util_2.NotImplemented, + networkingBgpAnnounceSetList: util_2.NotImplemented, + networkingBgpConfigCreate: util_2.NotImplemented, + networkingBgpConfigDelete: util_2.NotImplemented, + networkingBgpConfigList: util_2.NotImplemented, + networkingBgpImportedRoutesIpv4: util_2.NotImplemented, + networkingBgpStatus: util_2.NotImplemented, + networkingLoopbackAddressCreate: util_2.NotImplemented, + networkingLoopbackAddressDelete: util_2.NotImplemented, + networkingLoopbackAddressList: util_2.NotImplemented, + networkingSwitchPortApplySettings: util_2.NotImplemented, + networkingSwitchPortClearSettings: util_2.NotImplemented, + networkingSwitchPortList: util_2.NotImplemented, + networkingSwitchPortSettingsCreate: util_2.NotImplemented, + networkingSwitchPortSettingsDelete: util_2.NotImplemented, + networkingSwitchPortSettingsView: util_2.NotImplemented, + networkingSwitchPortSettingsList: util_2.NotImplemented, + rackView: util_2.NotImplemented, + roleList: util_2.NotImplemented, + roleView: util_2.NotImplemented, + siloPolicyUpdate: util_2.NotImplemented, + siloPolicyView: util_2.NotImplemented, + siloQuotasUpdate: util_2.NotImplemented, + siloQuotasView: util_2.NotImplemented, + siloUserList: util_2.NotImplemented, + siloUserView: util_2.NotImplemented, + sledAdd: util_2.NotImplemented, + sledListUninitialized: util_2.NotImplemented, + sledSetProvisionState: util_2.NotImplemented, + switchList: util_2.NotImplemented, + switchView: util_2.NotImplemented, + systemPolicyUpdate: util_2.NotImplemented, + systemQuotasList: util_2.NotImplemented, + userBuiltinList: util_2.NotImplemented, + userBuiltinView: util_2.NotImplemented, +}); diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index f50aacddbc..5cf79905ff 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -16,7 +16,6 @@ import { INSTANCE_MIN_RAM_GiB, MAX_NICS_PER_INSTANCE, type ApiTypes as Api, - type FloatingIp, type SamlIdentityProvider, } from '@oxide/api' import { json, makeHandlers, type Json } from '@oxide/gen/msw-handlers' @@ -226,12 +225,12 @@ export const handlers = makeHandlers({ }, floatingIpCreate({ body, query }) { const project = lookup.project(query) - errIfExists(db.vpcs, { name: body.name }) + errIfExists(db.floatingIps, { name: body.name }) const newFloatingIp: Json = { id: uuid(), project_id: project.id, - ip: body.address || '12.34.56.7', + ip: `${[...Array(4)].map(() => Math.floor(Math.random() * 256)).join('.')}`, ...body, ...getTimestamps(), } @@ -256,27 +255,20 @@ export const handlers = makeHandlers({ return 204 }, - floatingIpAttach({ body, path, query }) { + floatingIpAttach({ path, query, body }) { const floatingIp = lookup.floatingIp({ ...path, ...query }) - const instance = lookup.instance({ instance: body.parent }) - console.log(instance) - + const instance = lookup.instance({ ...path, ...query, instance: body.parent }) floatingIp.instance_id = instance.id - console.log(floatingIp) - return floatingIp }, floatingIpDetach({ path, query }) { - const floatingIp: FloatingIp = lookup.floatingIp({ ...path, ...query }) - db.floatingIps = db.floatingIps.map((ip) => { - if (ip.id !== floatingIp.id) { - return ip - } - return { ...ip, instance_id: undefined } - }) + const floatingIp = lookup.floatingIp({ ...path, ...query }) + db.floatingIps = db.floatingIps.map((ip) => + ip.id !== floatingIp.id ? ip : { ...ip, instance_id: undefined } + ) - return 204 + return floatingIp }, imageList({ query }) { if (query.project) { diff --git a/libs/api-mocks/msw/util.js b/libs/api-mocks/msw/util.js new file mode 100644 index 0000000000..6c061c0049 --- /dev/null +++ b/libs/api-mocks/msw/util.js @@ -0,0 +1,314 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.requireRole = exports.requireFleetViewer = exports.userHasRole = exports.currentUser = exports.MSW_USER_COOKIE = exports.handleMetrics = exports.generateUtilization = exports.errIfInvalidDiskSize = exports.errIfExists = exports.internalError = exports.NotImplemented = exports.unavailableErr = exports.getTimestamps = exports.getStartAndEndTime = exports.repeat = exports.paginated = exports.json = void 0; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +var date_fns_1 = require("date-fns"); +var api_1 = require("@oxide/api"); +var msw_handlers_1 = require("@oxide/gen/msw-handlers"); +var util_1 = require("@oxide/util"); +var metrics_1 = require("../metrics"); +var db_1 = require("./db"); +var msw_handlers_2 = require("@oxide/gen/msw-handlers"); +Object.defineProperty(exports, "json", { enumerable: true, get: function () { return msw_handlers_2.json; } }); +var paginated = function (params, items) { + var _a = params || {}, _b = _a.limit, limit = _b === void 0 ? 100 : _b, pageToken = _a.pageToken; + var startIndex = pageToken ? items.findIndex(function (i) { return i.id === pageToken; }) : 0; + startIndex = startIndex < 0 ? 0 : startIndex; + if (startIndex > items.length) { + return { + items: [], + nextPage: null, + }; + } + if (limit + startIndex >= items.length) { + return { + items: items.slice(startIndex), + nextPage: null, + }; + } + return { + items: items.slice(startIndex, startIndex + limit), + nextPage: "".concat(items[startIndex + limit].id), + }; +}; +exports.paginated = paginated; +// make a bunch of copies of an object with different names and IDs. useful for +// testing pagination +var repeat = function (obj, n) { + return new Array(n).fill(0).map(function (_, i) { return (__assign(__assign({}, obj), { id: obj.id + i, name: obj.name + i })); }); +}; +exports.repeat = repeat; +function getStartAndEndTime(params) { + // if no start time or end time, give the last 24 hours. in this case the + // API will give all data available for the metric (paginated of course), + // so essentially we're pretending the last 24 hours just happens to be + // all the data. if we have an end time but no start time, same deal, pretend + // 24 hours before the given end time is where it starts + var now = new Date(); + var _a = params.endTime, endTime = _a === void 0 ? now : _a, _b = params.startTime, startTime = _b === void 0 ? (0, date_fns_1.subHours)(endTime, 24) : _b; + return { startTime: startTime, endTime: endTime }; +} +exports.getStartAndEndTime = getStartAndEndTime; +function getTimestamps() { + var now = new Date().toISOString(); + return { time_created: now, time_modified: now }; +} +exports.getTimestamps = getTimestamps; +var unavailableErr = function () { + return (0, msw_handlers_1.json)({ error_code: 'ServiceUnavailable' }, { status: 503 }); +}; +exports.unavailableErr = unavailableErr; +var NotImplemented = function () { + // This doesn't just return the response because it broadens the type to be usable + // directly as a handler + throw (0, msw_handlers_1.json)({ error_code: 'NotImplemented' }, { status: 501 }); +}; +exports.NotImplemented = NotImplemented; +var internalError = function () { return (0, msw_handlers_1.json)({ error_code: 'InternalError' }, { status: 500 }); }; +exports.internalError = internalError; +var errIfExists = function (collection, match, resourceLabel) { + if (resourceLabel === void 0) { resourceLabel = 'resource'; } + if (collection.some(function (item) { + return Object.entries(match).every(function (_a) { + var key = _a[0], value = _a[1]; + return item[key] === value; + }); + })) { + var name_1 = 'name' in match ? match.name : 'id' in match ? match.id : ''; + throw (0, msw_handlers_1.json)({ + error_code: 'ObjectAlreadyExists', + message: "already exists: ".concat(resourceLabel, " \"").concat(name_1, "\""), + }, { status: 400 }); + } +}; +exports.errIfExists = errIfExists; +var errIfInvalidDiskSize = function (disk) { + var _a, _b, _c, _d; + var source = disk.disk_source; + if (disk.size < api_1.MIN_DISK_SIZE_GiB * util_1.GiB) { + throw "Disk size must be greater than or equal to ".concat(api_1.MIN_DISK_SIZE_GiB, " GiB"); + } + if (disk.size > api_1.MAX_DISK_SIZE_GiB * util_1.GiB) { + throw "Disk size must be less than or equal to ".concat(api_1.MAX_DISK_SIZE_GiB, " GiB"); + } + if (source.type === 'snapshot') { + var snapshotSize = (_b = (_a = db_1.db.snapshots.find(function (s) { return source.snapshot_id === s.id; })) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0; + if (disk.size >= snapshotSize) + return; + throw 'Disk size must be greater than or equal to the snapshot size'; + } + if (source.type === 'image') { + var imageSize = (_d = (_c = db_1.db.images.find(function (i) { return source.image_id === i.id; })) === null || _c === void 0 ? void 0 : _c.size) !== null && _d !== void 0 ? _d : 0; + if (disk.size >= imageSize) + return; + throw 'Disk size must be greater than or equal to the image size'; + } +}; +exports.errIfInvalidDiskSize = errIfInvalidDiskSize; +var Rando = /** @class */ (function () { + function Rando(seed, a, c, m) { + if (a === void 0) { a = 1664525; } + if (c === void 0) { c = 1013904223; } + if (m === void 0) { m = Math.pow(2, 32); } + this.seed = seed; + this.a = a; + this.c = c; + this.m = m; + } + Rando.prototype.next = function () { + this.seed = (this.a * this.seed + this.c) % this.m; + return this.seed / this.m; + }; + return Rando; +}()); +function generateUtilization(metricName, startTime, endTime, sleds) { + // generate data from at most 90 days ago no matter how early start time is + var adjStartTime = new Date(Math.max(startTime.getTime(), Date.now() - 1000 * 60 * 60 * 24 * 90)); + var capacity = (0, api_1.totalCapacity)(sleds.map(function (s) { return ({ + usableHardwareThreads: s.usable_hardware_threads, + usablePhysicalRam: s.usable_physical_ram, + }); })); + var cap = metricName === 'cpus_provisioned' + ? capacity.cpu + : metricName === 'virtual_disk_space_provisioned' + ? capacity.disk_tib * util_1.TiB + : capacity.ram_gib * util_1.GiB; + var metricNameSeed = Array.from(metricName).reduce(function (acc, char) { return acc + char.charCodeAt(0); }, 0); + var rando = new Rando(adjStartTime.getTime() + metricNameSeed); + var diff = Math.abs((0, date_fns_1.differenceInSeconds)(adjStartTime, endTime)); + // How many quarter hour chunks in the date range + // Use that as how often to offset the data to seem + // more realistic + var timeInterval = diff / 900; + // If the data is the following length + var dataCount = 1000; + // How far along the array should we do something + var valueInterval = Math.floor(dataCount / timeInterval); + // Pick a reasonable start value + var startVal = 500; + var values = new Array(dataCount); + values[0] = startVal; + var x = 0; + for (var i = 1; i < values.length; i++) { + values[i] = values[i - 1]; + if (x === valueInterval) { + // Do something 3/4 of the time + var offset = 0; + var random = rando.next(); + var threshold = i < 250 || (i > 500 && i < 750) ? 1 : 0.375; + if (random < threshold) { + var amount = 50; + offset = Math.floor(random * amount); + if (random < threshold / 2.5) { + offset = offset * -1; + } + } + if (random > 0.72) { + values[i] += offset; + } + else { + values[i] = Math.max(values[i] - offset, 0); + } + x = 0; + } + else { + x++; + } + } + // Find the current maximum value in the generated data + var currentMax = Math.max.apply(Math, values); + // Normalize the data to sit within the range of 0 to overall capacity + var randomFactor = Math.random() * (1 - 0.33) + 0.33; + var normalizedValues = values.map(function (value) { + var v = (value / currentMax) * cap * randomFactor; + if (metricName === 'cpus_provisioned') { + // CPU utilization should be whole numbers + v = Math.floor(v); + } + return v; + }); + return normalizedValues; +} +exports.generateUtilization = generateUtilization; +function handleMetrics(_a) { + var metricName = _a.path.metricName, query = _a.query; + var _b = getStartAndEndTime(query), startTime = _b.startTime, endTime = _b.endTime; + if (endTime <= startTime) + return { items: [] }; + var dataPoints = generateUtilization(metricName, startTime, endTime, db_1.db.sleds); + // Important to remember (but probably not important enough to change) that + // this works quite differently from the real API, which is going to be + // querying clickhouse with some fixed set of data, and when it starts from + // the end (order == 'descending') it's going to get data points starting + // from the end. When it starts from the beginning it gets data points from + // the beginning. For our fake data, we just generate the same set of data + // points spanning the whole time range, then reverse the list if necessary + // and take the first N=limit data points. + var items = (0, metrics_1.genI64Data)(dataPoints, startTime, endTime); + if (query.order === 'descending') { + items.reverse(); + } + if (typeof query.limit === 'number') { + items = items.slice(0, query.limit); + } + return { items: items }; +} +exports.handleMetrics = handleMetrics; +exports.MSW_USER_COOKIE = 'msw-user'; +/** + * Look up user by display name in cookie. Return the first user if cookie empty + * or name not found. We're using display name to make it easier to set the + * cookie by hand, because there is no way yet to pick a user through the UI. + * + * If cookie is empty or name is not found, return the first user in the list, + * who has admin on everything. + */ +function currentUser(cookies) { + var _a; + var name = cookies[exports.MSW_USER_COOKIE]; + return (_a = db_1.db.users.find(function (u) { return u.display_name === name; })) !== null && _a !== void 0 ? _a : db_1.db.users[0]; +} +exports.currentUser = currentUser; +/** + * Given a role A, get a list of the roles (including A) that confer *at least* + * the powers of A. + */ +// could implement with `takeUntil(allRoles, r => r === role)`, but that is so +// much harder to understand +var roleOrStronger = { + viewer: ['viewer', 'collaborator', 'admin'], + collaborator: ['collaborator', 'admin'], + admin: ['admin'], +}; +/** + * Determine whether a user has a role at least as strong as `role` on the + * specified resource. Note that this does not yet do parent-child inheritance + * like Nexus does, i.e., if a user has collaborator on a silo, then it inherits + * collaborator on all projects in the silo even if it has no explicit role on + * those projects. This does NOT do that. + */ +function userHasRole(user, resourceType, resourceId, role) { + var userGroupIds = db_1.db.groupMemberships + .filter(function (gm) { return gm.userId === user.id; }) + .map(function (gm) { return db_1.db.userGroups.find(function (g) { return g.id === gm.groupId; }); }) + .filter(util_1.isTruthy) + .map(function (g) { return g.id; }); + /** All actors with *at least* the specified role on the resource */ + var actorsWithRole = db_1.db.roleAssignments + .filter(function (ra) { + return ra.resource_type === resourceType && + ra.resource_id === resourceId && + roleOrStronger[role].includes(ra.role_name); + }) + .map(function (ra) { return ra.identity_id; }); + // user has role if their own ID or any of their groups is associated with the role + return __spreadArray([user.id], userGroupIds, true).some(function (id) { return actorsWithRole.includes(id); }); +} +exports.userHasRole = userHasRole; +/** + * Determine whether current user has fleet viewer permissions by looking for + * fleet roles for the user as well as for the user's groups. Do nothing if yes, + * throw 403 if no. + */ +function requireFleetViewer(cookies) { + requireRole(cookies, 'fleet', api_1.FLEET_ID, 'viewer'); +} +exports.requireFleetViewer = requireFleetViewer; +/** + * Determine whether current user has a role on a resource by looking roles + * for the user as well as for the user's groups. Do nothing if yes, throw 403 + * if no. + */ +function requireRole(cookies, resourceType, resourceId, role) { + var user = currentUser(cookies); + // should it 404? I think the API is a mix + if (!userHasRole(user, resourceType, resourceId, role)) + throw 403; +} +exports.requireRole = requireRole; diff --git a/libs/api-mocks/network-interface.js b/libs/api-mocks/network-interface.js new file mode 100644 index 0000000000..99afe903e1 --- /dev/null +++ b/libs/api-mocks/network-interface.js @@ -0,0 +1,18 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.networkInterface = void 0; +var instance_1 = require("./instance"); +var vpc_1 = require("./vpc"); +exports.networkInterface = { + id: 'f6d63297-287c-4035-b262-e8303cfd6a0f', + name: 'my-nic', + description: 'a network interface', + primary: true, + instance_id: instance_1.instance.id, + ip: '172.30.0.10', + mac: '', + subnet_id: vpc_1.vpcSubnet.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + vpc_id: vpc_1.vpc.id, +}; diff --git a/libs/api-mocks/physical-disk.js b/libs/api-mocks/physical-disk.js new file mode 100644 index 0000000000..5877f736c9 --- /dev/null +++ b/libs/api-mocks/physical-disk.js @@ -0,0 +1,45 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.physicalDisks = void 0; +exports.physicalDisks = [ + { + id: 'd2cf9763-cfce-4291-8531-614c8b4aa632', + form_factor: 'u2', + model: 'MTFDKBG800TDZ-1AZ1ZAB', + serial: '0C98MRMBK64', + vendor: '0634', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + sled_id: 'c2519937-44a4-493b-9b38-5c337c597d08', + }, + { + id: '9d43adfe-c46a-4a33-b060-146cbd48b767', + form_factor: 'u2', + model: 'MTFDKBG800TDZ-1AZ1ZAB', + serial: 'A9GCW7OS3HT', + vendor: '0634', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + sled_id: 'c2519937-44a4-493b-9b38-5c337c597d08', + }, + { + id: 'f253c29f-321b-46d0-a132-6235cc63e3d2', + form_factor: 'u2', + model: 'MTFDKBG800TDZ-1AZ1ZAB', + serial: '0V2L160OZ9J', + vendor: '0634', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + sled_id: 'c2519937-44a4-493b-9b38-5c337c597d08', + }, + { + id: '0591ae13-3c72-4701-a801-20e44f809496', + form_factor: 'm2', + model: 'MTFDKBG800TDZ-1AZ1ZAB', + serial: 'CA73ANUYLJ9', + vendor: '0634', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + sled_id: 'c2519937-44a4-493b-9b38-5c337c597d08', + }, +]; diff --git a/libs/api-mocks/project.js b/libs/api-mocks/project.js new file mode 100644 index 0000000000..2d8dbcf501 --- /dev/null +++ b/libs/api-mocks/project.js @@ -0,0 +1,28 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.projectRolePolicy = exports.projects = exports.project2 = exports.project = void 0; +var user_1 = require("./user"); +exports.project = { + id: '5fbab865-3d09-4c16-a22f-ca9c312b0286', + name: 'mock-project', + description: 'a fake project', + time_created: new Date(2021, 0, 1).toISOString(), + time_modified: new Date(2021, 0, 2).toISOString(), +}; +exports.project2 = { + id: 'e7bd835e-831e-4257-b600-f1db32844c8c', + name: 'other-project', + description: 'another fake project', + time_created: new Date(2021, 0, 15).toISOString(), + time_modified: new Date(2021, 0, 16).toISOString(), +}; +exports.projects = [exports.project, exports.project2]; +exports.projectRolePolicy = { + role_assignments: [ + { + identity_id: user_1.user1.id, + identity_type: 'silo_user', + role_name: 'admin', + }, + ], +}; diff --git a/libs/api-mocks/rack.js b/libs/api-mocks/rack.js new file mode 100644 index 0000000000..3b51176588 --- /dev/null +++ b/libs/api-mocks/rack.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.racks = exports.rack = void 0; +exports.rack = { + id: '6fbafcc7-1626-4785-be65-e212f8ad66d0', + time_created: new Date(2021, 0, 1).toISOString(), + time_modified: new Date(2021, 0, 2).toISOString(), +}; +exports.racks = [exports.rack]; diff --git a/libs/api-mocks/role-assignment.js b/libs/api-mocks/role-assignment.js new file mode 100644 index 0000000000..81a09225a1 --- /dev/null +++ b/libs/api-mocks/role-assignment.js @@ -0,0 +1,52 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.roleAssignments = void 0; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +var api_1 = require("@oxide/api"); +var project_1 = require("./project"); +var silo_1 = require("./silo"); +var user_1 = require("./user"); +var user_group_1 = require("./user-group"); +exports.roleAssignments = [ + { + resource_type: 'fleet', + resource_id: api_1.FLEET_ID, + identity_id: user_1.user1.id, + identity_type: 'silo_user', + role_name: 'admin', + }, + { + resource_type: 'silo', + resource_id: silo_1.defaultSilo.id, + identity_id: user_group_1.userGroup3.id, + identity_type: 'silo_group', + role_name: 'collaborator', + }, + { + resource_type: 'silo', + resource_id: silo_1.defaultSilo.id, + identity_id: user_1.user1.id, + identity_type: 'silo_user', + role_name: 'admin', + }, + { + resource_type: 'project', + resource_id: project_1.project.id, + identity_id: user_1.user3.id, + identity_type: 'silo_user', + role_name: 'collaborator', + }, + { + resource_type: 'project', + resource_id: project_1.project.id, + identity_id: user_group_1.userGroup2.id, + identity_type: 'silo_group', + role_name: 'viewer', + }, +]; diff --git a/libs/api-mocks/serial.js b/libs/api-mocks/serial.js new file mode 100644 index 0000000000..49afc6900a --- /dev/null +++ b/libs/api-mocks/serial.js @@ -0,0 +1,3480 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.serial = void 0; +exports.serial = { + last_byte_offset: 7351, + data: [ + 27, + 91, + 50, + 74, + 27, + 91, + 48, + 49, + 59, + 48, + 49, + 72, + 27, + 91, + 61, + 51, + 104, + 27, + 91, + 50, + 74, + 27, + 91, + 48, + 49, + 59, + 48, + 49, + 72, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 53, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 55, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 53, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 55, + 109, + 27, + 91, + 52, + 48, + 109, + 66, + 100, + 115, + 68, + 120, + 101, + 58, + 32, + 108, + 111, + 97, + 100, + 105, + 110, + 103, + 32, + 66, + [111, 2], + 116, + [48, 3], + 49, + 32, + 34, + 85, + 69, + 70, + 73, + 32, + 34, + 32, + 102, + 114, + 111, + 109, + 32, + 80, + 99, + 105, + 82, + [111, 2], + 116, + 40, + 48, + 120, + 48, + 41, + 47, + 80, + 99, + 105, + 40, + 48, + 120, + 49, + 48, + 44, + 48, + 120, + 48, + 41, + 47, + 78, + 86, + 77, + 101, + 40, + 48, + 120, + 49, + 44, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 41, + 13, + 10, + 66, + 100, + 115, + 68, + 120, + 101, + 58, + 32, + 115, + 116, + 97, + 114, + 116, + 105, + 110, + 103, + 32, + 66, + [111, 2], + 116, + [48, 3], + 49, + 32, + 34, + 85, + 69, + 70, + 73, + 32, + 34, + 32, + 102, + 114, + 111, + 109, + 32, + 80, + 99, + 105, + 82, + [111, 2], + 116, + 40, + 48, + 120, + 48, + 41, + 47, + 80, + 99, + 105, + 40, + 48, + 120, + 49, + 48, + 44, + 48, + 120, + 48, + 41, + 47, + 78, + 86, + 77, + 101, + 40, + 48, + 120, + 49, + 44, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 45, + [48, 2], + 41, + 13, + 10, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 48, + 109, + 27, + 91, + 52, + 55, + 109, + 87, + 101, + 108, + 99, + 111, + 109, + 101, + 32, + 116, + 111, + 32, + 71, + 82, + 85, + 66, + 33, + 10, + 13, + 10, + 13, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 55, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 48, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 50, + 74, + 27, + 91, + 48, + 49, + 59, + 48, + 49, + 72, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 55, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 48, + 50, + 59, + 51, + 48, + 72, + 71, + 78, + 85, + 32, + 71, + 82, + 85, + 66, + [32, 2], + 118, + 101, + 114, + 115, + 105, + 111, + 110, + 32, + 50, + 46, + 48, + 54, + 10, + 13, + 10, + 13, + 27, + 91, + 48, + 52, + 59, + 48, + 50, + 72, + 218, + [196, 76], + 191, + 27, + 91, + 48, + 53, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 48, + 53, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 48, + 54, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 48, + 54, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 48, + 55, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 48, + 55, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 48, + 56, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 48, + 56, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 48, + 57, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 48, + 57, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 49, + 48, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 49, + 48, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + [49, 2], + 59, + 48, + 50, + 72, + 179, + 27, + 91, + [49, 2], + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 49, + 50, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 49, + 50, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 49, + 51, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 49, + 51, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 49, + 52, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 49, + 52, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 49, + 53, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 49, + 53, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 49, + 54, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 49, + 54, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 49, + 55, + 59, + 48, + 50, + 72, + 179, + 27, + 91, + 49, + 55, + 59, + 55, + 57, + 72, + 179, + 27, + 91, + 49, + 56, + 59, + 48, + 50, + 72, + 192, + [196, 76], + 217, + 27, + 91, + 49, + 57, + 59, + 48, + 50, + 72, + 27, + 91, + 50, + 48, + 59, + 48, + 50, + 72, + [32, 5], + 85, + 115, + 101, + 32, + 116, + 104, + 101, + 32, + 94, + 32, + 97, + 110, + 100, + 32, + 118, + 32, + 107, + 101, + 121, + 115, + 32, + 116, + 111, + 32, + 115, + 101, + 108, + 101, + 99, + 116, + 32, + 119, + 104, + 105, + 99, + 104, + 32, + 101, + 110, + 116, + 114, + 121, + 32, + 105, + 115, + 32, + 104, + 105, + 103, + 104, + 108, + 105, + 103, + 104, + 116, + 101, + 100, + 46, + [32, 10], + 10, + 13, + [32, 6], + 80, + 114, + 101, + [115, 2], + 32, + 101, + 110, + 116, + 101, + 114, + 32, + 116, + 111, + 32, + 98, + [111, 2], + 116, + 32, + 116, + 104, + 101, + 32, + 115, + 101, + 108, + 101, + 99, + 116, + 101, + 100, + 32, + 79, + 83, + 44, + 32, + 96, + 101, + 39, + 32, + 116, + 111, + 32, + 101, + 100, + 105, + 116, + 32, + 116, + 104, + 101, + 32, + 99, + 111, + [109, 2], + 97, + 110, + 100, + 115, + [32, 7], + 10, + 13, + [32, 6], + 98, + 101, + 102, + 111, + 114, + 101, + 32, + 98, + [111, 2], + 116, + 105, + 110, + 103, + 32, + 111, + 114, + 32, + 96, + 99, + 39, + 32, + 102, + 111, + 114, + 32, + 97, + 32, + 99, + 111, + [109, 2], + 97, + 110, + 100, + 45, + 108, + 105, + 110, + 101, + 46, + [32, 27], + 27, + 91, + 48, + 53, + 59, + 56, + 48, + 72, + 32, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 48, + 109, + 27, + 91, + 52, + 55, + 109, + 27, + 91, + 48, + 53, + 59, + 48, + 51, + 72, + 42, + 76, + 105, + 110, + 117, + 120, + 32, + 118, + 105, + 114, + 116, + [32, 65], + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 55, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 48, + 53, + 59, + 55, + 56, + 72, + 27, + 91, + 48, + 54, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 48, + 54, + 59, + 55, + 56, + 72, + 27, + 91, + 48, + 55, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 48, + 55, + 59, + 55, + 56, + 72, + 27, + 91, + 48, + 56, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 48, + 56, + 59, + 55, + 56, + 72, + 27, + 91, + 48, + 57, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 48, + 57, + 59, + 55, + 56, + 72, + 27, + 91, + 49, + 48, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 49, + 48, + 59, + 55, + 56, + 72, + 27, + 91, + [49, 2], + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + [49, 2], + 59, + 55, + 56, + 72, + 27, + 91, + 49, + 50, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 49, + 50, + 59, + 55, + 56, + 72, + 27, + 91, + 49, + 51, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 49, + 51, + 59, + 55, + 56, + 72, + 27, + 91, + 49, + 52, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 49, + 52, + 59, + 55, + 56, + 72, + 27, + 91, + 49, + 53, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 49, + 53, + 59, + 55, + 56, + 72, + 27, + 91, + 49, + 54, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 49, + 54, + 59, + 55, + 56, + 72, + 27, + 91, + 49, + 55, + 59, + 48, + 51, + 72, + [32, 76], + 27, + 91, + 49, + 55, + 59, + 55, + 56, + 72, + 27, + 91, + 49, + 55, + 59, + 56, + 48, + 72, + 32, + 27, + 91, + 48, + 53, + 59, + 55, + 56, + 72, + 27, + 91, + 50, + 51, + 59, + 48, + 49, + 72, + [32, 3], + 84, + 104, + 101, + 32, + 104, + 105, + 103, + 104, + 108, + 105, + 103, + 104, + 116, + 101, + 100, + 32, + 101, + 110, + 116, + 114, + 121, + 32, + 119, + 105, + [108, 2], + 32, + 98, + 101, + 32, + 101, + 120, + 101, + 99, + 117, + 116, + 101, + 100, + 32, + 97, + 117, + 116, + 111, + 109, + 97, + 116, + 105, + 99, + 97, + [108, 2], + 121, + 32, + 105, + 110, + 32, + 49, + 115, + 46, + [32, 17], + 27, + 91, + 48, + 53, + 59, + 55, + 56, + 72, + 27, + 91, + 50, + 51, + 59, + 48, + 49, + 72, + [32, 3], + 84, + 104, + 101, + 32, + 104, + 105, + 103, + 104, + 108, + 105, + 103, + 104, + 116, + 101, + 100, + 32, + 101, + 110, + 116, + 114, + 121, + 32, + 119, + 105, + [108, 2], + 32, + 98, + 101, + 32, + 101, + 120, + 101, + 99, + 117, + 116, + 101, + 100, + 32, + 97, + 117, + 116, + 111, + 109, + 97, + 116, + 105, + 99, + 97, + [108, 2], + 121, + 32, + 105, + 110, + 32, + 48, + 115, + 46, + [32, 17], + 27, + 91, + 48, + 53, + 59, + 55, + 56, + 72, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 48, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 50, + 74, + 27, + 91, + 48, + 49, + 59, + 48, + 49, + 72, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 55, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 48, + 109, + 27, + 91, + 52, + 48, + 109, + 27, + 91, + 50, + 74, + 27, + 91, + 48, + 49, + 59, + 48, + 49, + 72, + 27, + 91, + 48, + 109, + 27, + 91, + 51, + 55, + 109, + 27, + 91, + 52, + 48, + 109, + [32, 2], + 66, + [111, 2], + 116, + 105, + 110, + 103, + 32, + 96, + 76, + 105, + 110, + 117, + 120, + 32, + 118, + 105, + 114, + 116, + 39, + 10, + 13, + 10, + 13, + 27, + 55, + [32, 2], + 48, + 37, + [32, 45], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + [32, 2], + 54, + 37, + 32, + [35, 3], + [32, 41], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 49, + 53, + 37, + 32, + [35, 6], + [32, 38], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 49, + 54, + 37, + 32, + [35, 7], + [32, 37], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 49, + 57, + 37, + 32, + [35, 8], + [32, 36], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 48, + 37, + 32, + [35, 9], + [32, 35], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 49, + 37, + 32, + [35, 9], + [32, 35], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + [50, 2], + 37, + 32, + [35, 9], + [32, 35], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + [50, 2], + 37, + 32, + [35, 10], + [32, 34], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 51, + 37, + 32, + [35, 10], + [32, 34], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 52, + 37, + 32, + [35, 10], + [32, 34], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 53, + 37, + 32, + [35, 11], + [32, 33], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 54, + 37, + 32, + [35, 11], + [32, 33], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 55, + 37, + 32, + [35, 11], + [32, 33], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 55, + 37, + 32, + [35, 12], + [32, 32], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 56, + 37, + 32, + [35, 12], + [32, 32], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 57, + 37, + 32, + [35, 12], + [32, 32], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 50, + 57, + 37, + 32, + [35, 13], + [32, 31], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 48, + 37, + 32, + [35, 13], + [32, 31], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 49, + 37, + 32, + [35, 13], + [32, 31], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 49, + 37, + 32, + [35, 14], + [32, 30], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 50, + 37, + 32, + [35, 14], + [32, 30], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + [51, 2], + 37, + 32, + [35, 14], + [32, 30], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 52, + 37, + 32, + [35, 15], + [32, 29], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 53, + 37, + 32, + [35, 15], + [32, 29], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 54, + 37, + 32, + [35, 15], + [32, 29], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 54, + 37, + 32, + [35, 16], + [32, 28], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 55, + 37, + 32, + [35, 16], + [32, 28], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 56, + 37, + 32, + [35, 16], + [32, 28], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 56, + 37, + 32, + [35, 17], + [32, 27], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 51, + 57, + 37, + 32, + [35, 17], + [32, 27], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 48, + 37, + 32, + [35, 17], + [32, 27], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 48, + 37, + 32, + [35, 18], + [32, 26], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 49, + 37, + 32, + [35, 18], + [32, 26], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 50, + 37, + 32, + [35, 18], + [32, 26], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 51, + 37, + 32, + [35, 19], + [32, 25], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 53, + 37, + 32, + [35, 19], + [32, 25], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 53, + 37, + 32, + [35, 20], + [32, 24], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 54, + 37, + 32, + [35, 20], + [32, 24], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 55, + 37, + 32, + [35, 20], + [32, 24], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 55, + 37, + 32, + [35, 21], + [32, 23], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 52, + 57, + 37, + 32, + [35, 21], + [32, 23], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 53, + 48, + 37, + 32, + [35, 22], + [32, 22], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 55, + 49, + 37, + 32, + [35, 31], + [32, 13], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 55, + 54, + 37, + 32, + [35, 33], + [32, 11], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + [55, 2], + 37, + 32, + [35, 34], + [32, 10], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 56, + 50, + 37, + 32, + [35, 36], + [32, 8], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 56, + 51, + 37, + 32, + [35, 36], + [32, 8], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 56, + 52, + 37, + 32, + [35, 37], + [32, 7], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 56, + 54, + 37, + 32, + [35, 37], + [32, 7], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 56, + 55, + 37, + 32, + [35, 38], + [32, 6], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + [56, 2], + 37, + 32, + [35, 38], + [32, 6], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + [56, 2], + 37, + 32, + [35, 39], + [32, 5], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 56, + 57, + 37, + 32, + [35, 39], + [32, 5], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 57, + 48, + 37, + 32, + [35, 39], + [32, 5], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 57, + 48, + 37, + 32, + [35, 40], + [32, 4], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 57, + 49, + 37, + 32, + [35, 40], + [32, 4], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 57, + 50, + 37, + 32, + [35, 40], + [32, 4], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 57, + 51, + 37, + 32, + [35, 40], + [32, 4], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 32, + 57, + 51, + 37, + 32, + [35, 41], + [32, 3], + 27, + 56, + 27, + 91, + 48, + 75, + 27, + 55, + 49, + [48, 2], + 37, + 32, + [35, 44], + 27, + 56, + 27, + 91, + 48, + 75, + 13, + 10, + 13, + 10, + [32, 3], + 79, + 112, + 101, + 110, + 82, + 67, + 32, + 48, + 46, + [52, 2], + 46, + 49, + 48, + 32, + 105, + 115, + 32, + 115, + 116, + 97, + 114, + 116, + 105, + 110, + 103, + 32, + 117, + 112, + 32, + 76, + 105, + 110, + 117, + 120, + 32, + 53, + 46, + 49, + 53, + 46, + 52, + 49, + 45, + 48, + 45, + 118, + 105, + 114, + 116, + 32, + 40, + 120, + 56, + 54, + 95, + 54, + 52, + 41, + 13, + 10, + 13, + 10, + 32, + 42, + 32, + 47, + 112, + 114, + 111, + 99, + 32, + 105, + 115, + 32, + 97, + 108, + 114, + 101, + 97, + 100, + 121, + 32, + 109, + 111, + 117, + 110, + 116, + 101, + 100, + 13, + 10, + 32, + 42, + 32, + 77, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 47, + 114, + 117, + 110, + 32, + [46, 3], + 32, + 42, + 32, + 47, + 114, + 117, + 110, + 47, + 111, + 112, + 101, + 110, + 114, + 99, + 58, + 32, + 99, + 114, + 101, + 97, + 116, + 105, + 110, + 103, + 32, + 100, + 105, + 114, + 101, + 99, + 116, + 111, + 114, + 121, + 13, + 10, + 32, + 42, + 32, + 47, + 114, + 117, + 110, + 47, + 108, + 111, + 99, + 107, + 58, + 32, + 99, + 114, + 101, + 97, + 116, + 105, + 110, + 103, + 32, + 100, + 105, + 114, + 101, + 99, + 116, + 111, + 114, + 121, + 13, + 10, + 32, + 42, + 32, + 47, + 114, + 117, + 110, + 47, + 108, + 111, + 99, + 107, + 58, + 32, + 99, + 111, + [114, 2], + 101, + 99, + 116, + 105, + 110, + 103, + 32, + 111, + 119, + 110, + 101, + 114, + 13, + 10, + 32, + 42, + 32, + 67, + 97, + 99, + 104, + 105, + 110, + 103, + 32, + 115, + 101, + 114, + 118, + 105, + 99, + 101, + 32, + 100, + 101, + 112, + 101, + 110, + 100, + 101, + 110, + 99, + 105, + 101, + 115, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 82, + 101, + 109, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 100, + 101, + 118, + 116, + 109, + 112, + 102, + 115, + 32, + 111, + 110, + 32, + 47, + 100, + 101, + 118, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 77, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 47, + 100, + 101, + 118, + 47, + 109, + 113, + 117, + 101, + 117, + 101, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 77, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 109, + 111, + 100, + 108, + [111, 2], + 112, + [32, 2], + [46, 3], + 32, + 42, + 32, + 86, + 101, + 114, + 105, + 102, + 121, + 105, + 110, + 103, + 32, + 109, + 111, + 100, + 108, + [111, 2], + 112, + 13, + 10, + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 77, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 115, + 101, + 99, + 117, + 114, + 105, + 116, + 121, + 32, + 102, + 105, + 108, + 101, + 115, + 121, + 115, + 116, + 101, + 109, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 77, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 100, + 101, + 98, + 117, + 103, + 32, + 102, + 105, + 108, + 101, + 115, + 121, + 115, + 116, + 101, + 109, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 77, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 112, + 101, + 114, + 115, + 105, + 115, + 116, + 101, + 110, + 116, + 32, + 115, + 116, + 111, + 114, + 97, + 103, + 101, + 32, + 40, + 112, + 115, + 116, + 111, + 114, + 101, + 41, + 32, + 102, + 105, + 108, + 101, + 115, + 121, + 115, + 116, + 101, + 109, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 77, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 101, + 102, + 105, + 118, + 97, + 114, + 102, + 115, + 32, + 102, + 105, + 108, + 101, + 115, + 121, + 115, + 116, + 101, + 109, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 83, + 116, + 97, + 114, + 116, + 105, + 110, + 103, + 32, + 98, + 117, + 115, + 121, + 98, + 111, + 120, + 32, + 109, + 100, + 101, + 118, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 76, + 111, + 97, + 100, + 105, + 110, + 103, + 32, + 104, + 97, + 114, + 100, + 119, + 97, + 114, + 101, + 32, + 100, + 114, + 105, + 118, + 101, + 114, + 115, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 76, + 111, + 97, + 100, + 105, + 110, + 103, + 32, + 109, + 111, + 100, + 117, + 108, + 101, + 115, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 83, + 101, + [116, 2], + 105, + 110, + 103, + 32, + 115, + 121, + 115, + 116, + 101, + 109, + 32, + 99, + 108, + 111, + 99, + 107, + 32, + 117, + 115, + 105, + 110, + 103, + 32, + 116, + 104, + 101, + 32, + 104, + 97, + 114, + 100, + 119, + 97, + 114, + 101, + 32, + 99, + 108, + 111, + 99, + 107, + 32, + 91, + 85, + 84, + 67, + 93, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 67, + 104, + 101, + 99, + 107, + 105, + 110, + 103, + 32, + 108, + 111, + 99, + 97, + 108, + 32, + 102, + 105, + 108, + 101, + 115, + 121, + 115, + 116, + 101, + 109, + 115, + [32, 2], + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 82, + 101, + 109, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 102, + 105, + 108, + 101, + 115, + 121, + 115, + 116, + 101, + 109, + 115, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 77, + 111, + 117, + 110, + 116, + 105, + 110, + 103, + 32, + 108, + 111, + 99, + 97, + 108, + 32, + 102, + 105, + 108, + 101, + 115, + 121, + 115, + 116, + 101, + 109, + 115, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 67, + 111, + 110, + 102, + 105, + 103, + 117, + 114, + 105, + 110, + 103, + 32, + 107, + 101, + 114, + 110, + 101, + 108, + 32, + 112, + 97, + 114, + 97, + 109, + 101, + 116, + 101, + 114, + 115, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 77, + 105, + 103, + 114, + 97, + 116, + 105, + 110, + 103, + 32, + 47, + 118, + 97, + 114, + 47, + 108, + 111, + 99, + 107, + 32, + 116, + 111, + 32, + 47, + 114, + 117, + 110, + 47, + 108, + 111, + 99, + 107, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 67, + 114, + 101, + 97, + 116, + 105, + 110, + 103, + 32, + 117, + 115, + 101, + 114, + 32, + 108, + 111, + 103, + 105, + 110, + 32, + 114, + 101, + 99, + 111, + 114, + 100, + 115, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 67, + 108, + 101, + 97, + 110, + 105, + 110, + 103, + 32, + 47, + 116, + 109, + 112, + 32, + 100, + 105, + 114, + 101, + 99, + 116, + 111, + 114, + 121, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 83, + 101, + [116, 2], + 105, + 110, + 103, + 32, + 104, + 111, + 115, + 116, + 110, + 97, + 109, + 101, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 83, + 116, + 97, + 114, + 116, + 105, + 110, + 103, + 32, + 98, + 117, + 115, + 121, + 98, + 111, + 120, + 32, + 115, + 121, + 115, + 108, + 111, + 103, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + 32, + 42, + 32, + 83, + 116, + 97, + 114, + 116, + 105, + 110, + 103, + 32, + 102, + 105, + 114, + 115, + 116, + 98, + [111, 2], + 116, + 32, + [46, 3], + 32, + 91, + 32, + 111, + 107, + 32, + 93, + 13, + 10, + [13, 2], + 10, + 87, + 101, + 108, + 99, + 111, + 109, + 101, + 32, + 116, + 111, + 32, + 65, + 108, + 112, + 105, + 110, + 101, + 32, + 76, + 105, + 110, + 117, + 120, + 32, + 51, + 46, + 49, + 54, + 13, + 10, + 13, + 75, + 101, + 114, + 110, + 101, + 108, + 32, + 53, + 46, + 49, + 53, + 46, + 52, + 49, + 45, + 48, + 45, + 118, + 105, + 114, + 116, + 32, + 111, + 110, + 32, + 97, + 110, + 32, + 120, + 56, + 54, + 95, + 54, + 52, + 32, + 40, + 47, + 100, + 101, + 118, + 47, + [116, 2], + 121, + 83, + 48, + 41, + 13, + 10, + [13, 2], + 10, + 13, + 108, + 111, + 99, + 97, + 108, + 104, + 111, + 115, + 116, + 32, + 108, + 111, + 103, + 105, + 110, + 58, + 32, + ].flatMap(function (v) { return (Array.isArray(v) ? Array(v[1]).fill(v[0]) : v); }), +}; diff --git a/libs/api-mocks/silo.js b/libs/api-mocks/silo.js new file mode 100644 index 0000000000..e09de742e8 --- /dev/null +++ b/libs/api-mocks/silo.js @@ -0,0 +1,96 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.toIdp = exports.identityProviders = exports.samlIdp = exports.siloProvisioned = exports.siloQuotas = exports.defaultSilo = exports.silos = void 0; +var util_1 = require("@oxide/util"); +exports.silos = [ + { + id: '6d3a9c06-475e-4f75-b272-c0d0e3f980fa', + name: 'maze-war', + description: 'a silo', + time_created: new Date(2021, 3, 1).toISOString(), + time_modified: new Date(2021, 4, 2).toISOString(), + discoverable: true, + identity_mode: 'saml_jit', + mapped_fleet_roles: { + admin: ['admin'], + }, + }, + { + id: '68b58556-15b9-4ccb-adff-9fd3c7de1f9a', + name: 'myriad', + description: 'a second silo', + time_created: new Date(2023, 1, 28).toISOString(), + time_modified: new Date(2023, 6, 12).toISOString(), + discoverable: true, + identity_mode: 'saml_jit', + mapped_fleet_roles: {}, + }, +]; +exports.defaultSilo = exports.silos[0]; +exports.siloQuotas = [ + { + silo_id: exports.silos[0].id, + cpus: 50, + memory: 300 * util_1.GiB, + storage: 7 * util_1.TiB, + }, + { + silo_id: exports.silos[1].id, + cpus: 34, + memory: 500 * util_1.GiB, + storage: 9 * util_1.TiB, + }, +]; +// unlike siloQuotas, this doesn't exactly match how it's done in Nexus, but +// it's good enough. All we need is to be able to pull the provisioned amounts +// for a given silo. Note it has the same shape as the quotas object. +exports.siloProvisioned = [ + { + silo_id: exports.silos[0].id, + cpus: 30, + memory: 234 * util_1.GiB, + storage: 4.3 * util_1.TiB, + }, + { + silo_id: exports.silos[1].id, + cpus: 8, + memory: 150 * util_1.GiB, + storage: 2 * util_1.TiB, + }, +]; +exports.samlIdp = { + id: '2a96ce6f-c178-4631-9cde-607d65b539c7', + description: 'An identity provider but what if it had a really long description', + name: 'mock-idp', + time_created: new Date(2021, 4, 3, 4).toISOString(), + time_modified: new Date(2021, 4, 3, 5).toISOString(), + acs_url: '', + idp_entity_id: '', + public_cert: '', + slo_url: '', + sp_client_id: '', + technical_contact_email: '', +}; +exports.identityProviders = [ + { type: 'saml', siloId: exports.defaultSilo.id, provider: exports.samlIdp }, +]; +/** + * Extract generic `IdentityProvider` from a specific `*IdentityProvider` + * type like `SamlIdentityProvider` + */ +var toIdp = function (_a) { + var provider = _a.provider, type = _a.type; + return (__assign({ provider_type: type }, (0, util_1.pick)(provider, 'id', 'name', 'description', 'time_created', 'time_modified'))); +}; +exports.toIdp = toIdp; diff --git a/libs/api-mocks/sled.js b/libs/api-mocks/sled.js new file mode 100644 index 0000000000..4909c48df4 --- /dev/null +++ b/libs/api-mocks/sled.js @@ -0,0 +1,18 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sleds = exports.sled = void 0; +exports.sled = { + id: 'c2519937-44a4-493b-9b38-5c337c597d08', + time_created: new Date(2021, 0, 1).toISOString(), + time_modified: new Date(2021, 0, 2).toISOString(), + rack_id: '6fbafcc7-1626-4785-be65-e212f8ad66d0', + provision_state: 'provisionable', + baseboard: { + part: '913-0000008', + serial: 'BRM02222867', + revision: 0, + }, + usable_hardware_threads: 128, + usable_physical_ram: 1099511627776, +}; +exports.sleds = [exports.sled]; diff --git a/libs/api-mocks/snapshot.js b/libs/api-mocks/snapshot.js new file mode 100644 index 0000000000..2ffcaef968 --- /dev/null +++ b/libs/api-mocks/snapshot.js @@ -0,0 +1,95 @@ +"use strict"; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.snapshots = void 0; +var uuid_1 = require("uuid"); +var disk_1 = require("./disk"); +var project_1 = require("./project"); +var generatedSnapshots = Array.from({ length: 25 }, function (_, i) { + return generateSnapshot(i); +}); +exports.snapshots = __spreadArray([ + { + id: 'ab805e59-b6b8-4c73-8081-6a224b6b0698', + name: 'snapshot-1', + description: "it's a snapshot", + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + size: 1024, + disk_id: disk_1.disks[0].id, + state: 'ready', + }, + { + id: '9a29813d-e94b-4c6a-82a0-672af3f78a6f', + name: 'snapshot-2', + description: "it's a second snapshot", + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + size: 2048, + disk_id: disk_1.disks[0].id, + state: 'ready', + }, + { + id: 'e6c58826-62fb-4205-820e-620407cd04e7', + name: 'delete-500', + description: "it's a third snapshot", + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + size: 3072, + disk_id: disk_1.disks[0].id, + state: 'ready', + }, + { + id: 'dc598369-4554-4ccd-aa89-a837e6ca487d', + name: 'snapshot-4', + description: "it's a fourth snapshot", + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + size: 4096, + disk_id: disk_1.disks[0].id, + state: 'ready', + }, + { + id: 'ca117fc6-d3e4-452e-9e1c-15abea752ff6', + name: 'snapshot-disk-deleted', + description: 'technically it never existed', + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + size: 5120, + disk_id: 'a6f61e3f-25c1-49b0-a013-ac6a2d98a948', + state: 'ready', + } +], generatedSnapshots, true); +function generateSnapshot(index) { + return { + id: (0, uuid_1.v4)(), + name: "disk-1-snapshot-".concat(index + 5), + description: '', + project_id: project_1.project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + size: 1024 * (index + 1), + disk_id: disk_1.disks[0].id, + state: 'ready', + }; +} diff --git a/libs/api-mocks/sshKeys.js b/libs/api-mocks/sshKeys.js new file mode 100644 index 0000000000..d52b6b8bbf --- /dev/null +++ b/libs/api-mocks/sshKeys.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sshKeys = void 0; +var user_1 = require("./user"); +exports.sshKeys = [ + { + id: '43af8bc5-6f8e-404d-8b39-72b07cc9da56', + name: 'm1-macbook-pro', + description: 'For use on personal projects', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + public_key: 'aslsddlfkjsdlfkjsdlfkjsdlfkjsdflkjsdlfkjsdlfkjsd', + silo_user_id: user_1.user1.id, + }, + { + id: 'b2c3d4e5-6f7g-8h9i-0j1k-2l3m4n5o6p7q', + name: 'mac-mini', + description: '', + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + public_key: 'aslsddlfkjsdlfkjsdlfkjsdlfkjsdflkjsdlfkjsdlfkjsd', + silo_user_id: user_1.user1.id, + }, +]; diff --git a/libs/api-mocks/user-group.js b/libs/api-mocks/user-group.js new file mode 100644 index 0000000000..e333e4a937 --- /dev/null +++ b/libs/api-mocks/user-group.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.groupMemberships = exports.userGroups = exports.userGroup3 = exports.userGroup2 = exports.userGroup1 = void 0; +var silo_1 = require("./silo"); +var user_1 = require("./user"); +exports.userGroup1 = { + id: '0ff6da96-5d6d-4326-b059-2b72c1b51457', + silo_id: silo_1.defaultSilo.id, + display_name: 'web-devs', +}; +exports.userGroup2 = { + id: '1b5fa004-a378-4225-960f-60f089684b05', + silo_id: silo_1.defaultSilo.id, + display_name: 'kernel-devs', +}; +exports.userGroup3 = { + id: '5e30797c-cae3-4402-aeb7-d5044c4bed29', + silo_id: silo_1.defaultSilo.id, + display_name: 'real-estate-devs', +}; +exports.userGroups = [exports.userGroup1, exports.userGroup2, exports.userGroup3]; +exports.groupMemberships = [ + { + userId: user_1.user1.id, + groupId: exports.userGroup1.id, + }, + { + userId: user_1.user2.id, + groupId: exports.userGroup3.id, + }, +]; diff --git a/libs/api-mocks/user.js b/libs/api-mocks/user.js new file mode 100644 index 0000000000..919c3fcb79 --- /dev/null +++ b/libs/api-mocks/user.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.users = exports.user4 = exports.user3 = exports.user2 = exports.user1 = void 0; +var silo_1 = require("./silo"); +exports.user1 = { + id: '2e28576d-43e0-4e9e-9132-838a7b66f602', + display_name: 'Hannah Arendt', + silo_id: silo_1.defaultSilo.id, +}; +exports.user2 = { + id: '6937b251-013c-4f96-9afc-c62b1318dd0b', + display_name: 'Hans Jonas', + silo_id: silo_1.defaultSilo.id, +}; +exports.user3 = { + id: '4962021b-35e1-44a7-a40c-2264cd540615', + display_name: 'Jacob Klein', + silo_id: silo_1.defaultSilo.id, +}; +exports.user4 = { + id: '37c6aa2f-899e-4d56-bad1-93b5526a7151', + display_name: 'Simone de Beauvoir', + silo_id: silo_1.defaultSilo.id, +}; +exports.users = [exports.user1, exports.user2, exports.user3, exports.user4]; diff --git a/libs/api-mocks/vpc.js b/libs/api-mocks/vpc.js new file mode 100644 index 0000000000..8f9227ec4f --- /dev/null +++ b/libs/api-mocks/vpc.js @@ -0,0 +1,117 @@ +"use strict"; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.defaultFirewallRules = exports.vpcSubnet2 = exports.vpcSubnet = exports.vpc = void 0; +var project_1 = require("./project"); +var time_created = new Date(2021, 0, 1).toISOString(); +var time_modified = new Date(2021, 0, 2).toISOString(); +var systemRouterId = 'b5af837b-b986-4a0a-b775-516d76c84ec3'; +exports.vpc = { + id: '87774ff3-c6c1-475b-b920-ba2954f390fe', + name: 'mock-vpc', + description: 'a fake vpc', + dns_name: 'mock-vpc', + project_id: project_1.project.id, + system_router_id: systemRouterId, + ipv6_prefix: 'fdf6:1818:b6e1::/48', + time_created: time_created, + time_modified: time_modified, +}; +exports.vpcSubnet = { + // this is supposed to be flattened into the top level. will fix in API + id: 'd12bf934-d2bf-40e9-8596-bb42a7793749', + name: 'mock-subnet', + description: 'a fake subnet', + time_created: new Date(2021, 0, 1).toISOString(), + time_modified: new Date(2021, 0, 2).toISOString(), + // supposed to be camelcase, will fix in API + vpc_id: exports.vpc.id, + ipv4_block: '10.1.1.1/24', + ipv6_block: 'fd9b:870a:4245::/64', +}; +exports.vpcSubnet2 = __assign(__assign({}, exports.vpcSubnet), { id: 'cb001986-1dbe-440c-8872-a769a5c3cda6', name: 'mock-subnet-2', vpc_id: exports.vpc.id, ipv4_block: '10.1.1.2/24' }); +exports.defaultFirewallRules = [ + { + id: 'b74aeea8-1201-4efd-b6ec-011f10a0b176', + name: 'allow-internal-inbound', + status: 'enabled', + direction: 'inbound', + targets: [{ type: 'vpc', value: 'default' }], + action: 'allow', + description: 'allow inbound traffic to all instances within the VPC if originated within the VPC', + filters: { + hosts: [{ type: 'vpc', value: 'default' }], + }, + priority: 65534, + time_created: time_created, + time_modified: time_modified, + vpc_id: exports.vpc.id, + }, + { + id: '9802cd8e-1e59-4fdf-9b40-99c189f7a19b', + name: 'allow-ssh', + status: 'enabled', + direction: 'inbound', + targets: [{ type: 'vpc', value: 'default' }], + description: 'allow inbound TCP connections on port 22 from anywhere', + filters: { + ports: ['22'], + protocols: ['TCP'], + }, + action: 'allow', + priority: 65534, + time_created: time_created, + time_modified: time_modified, + vpc_id: exports.vpc.id, + }, + { + id: 'cde07d86-b8c0-49ed-8754-55f1bdee20fe', + name: 'allow-icmp', + status: 'enabled', + direction: 'inbound', + targets: [{ type: 'vpc', value: 'default' }], + description: 'allow inbound ICMP traffic from anywhere', + filters: { + protocols: ['ICMP'], + }, + action: 'allow', + priority: 65534, + time_created: time_created, + time_modified: time_modified, + vpc_id: exports.vpc.id, + }, + { + id: '5ed562d9-2566-496d-b7b3-7976b04a0b80', + name: 'allow-rdp', + status: 'enabled', + direction: 'inbound', + targets: [{ type: 'vpc', value: 'default' }], + description: 'allow inbound TCP connections on port 3389 from anywhere', + filters: { + ports: ['3389'], + protocols: ['TCP'], + }, + action: 'allow', + priority: 65534, + time_created: time_created, + time_modified: time_modified, + vpc_id: exports.vpc.id, + }, +]; From 0ebb041784f9b8ba9d7989e2aaafa20171f905e7 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 14 Feb 2024 16:31:29 -0800 Subject: [PATCH 28/64] Remove js files accidentally included in branch --- libs/api-mocks/disk.js | 140 -- libs/api-mocks/external-ip.js | 37 - libs/api-mocks/floating-ip.js | 34 - libs/api-mocks/image.js | 30 - libs/api-mocks/index.js | 50 - libs/api-mocks/instance.js | 44 - libs/api-mocks/ip-pool.js | 74 - libs/api-mocks/json-type.js | 12 - libs/api-mocks/metrics.js | 37 - libs/api-mocks/msw/db.js | 307 --- libs/api-mocks/msw/handlers.js | 1107 --------- libs/api-mocks/msw/util.js | 314 --- libs/api-mocks/network-interface.js | 18 - libs/api-mocks/physical-disk.js | 45 - libs/api-mocks/project.js | 28 - libs/api-mocks/rack.js | 9 - libs/api-mocks/role-assignment.js | 52 - libs/api-mocks/serial.js | 3480 --------------------------- libs/api-mocks/silo.js | 96 - libs/api-mocks/sled.js | 18 - libs/api-mocks/snapshot.js | 95 - libs/api-mocks/sshKeys.js | 24 - libs/api-mocks/user-group.js | 31 - libs/api-mocks/user.js | 25 - libs/api-mocks/vpc.js | 117 - 25 files changed, 6224 deletions(-) delete mode 100644 libs/api-mocks/disk.js delete mode 100644 libs/api-mocks/external-ip.js delete mode 100644 libs/api-mocks/floating-ip.js delete mode 100644 libs/api-mocks/image.js delete mode 100644 libs/api-mocks/index.js delete mode 100644 libs/api-mocks/instance.js delete mode 100644 libs/api-mocks/ip-pool.js delete mode 100644 libs/api-mocks/json-type.js delete mode 100644 libs/api-mocks/metrics.js delete mode 100644 libs/api-mocks/msw/db.js delete mode 100644 libs/api-mocks/msw/handlers.js delete mode 100644 libs/api-mocks/msw/util.js delete mode 100644 libs/api-mocks/network-interface.js delete mode 100644 libs/api-mocks/physical-disk.js delete mode 100644 libs/api-mocks/project.js delete mode 100644 libs/api-mocks/rack.js delete mode 100644 libs/api-mocks/role-assignment.js delete mode 100644 libs/api-mocks/serial.js delete mode 100644 libs/api-mocks/silo.js delete mode 100644 libs/api-mocks/sled.js delete mode 100644 libs/api-mocks/snapshot.js delete mode 100644 libs/api-mocks/sshKeys.js delete mode 100644 libs/api-mocks/user-group.js delete mode 100644 libs/api-mocks/user.js delete mode 100644 libs/api-mocks/vpc.js diff --git a/libs/api-mocks/disk.js b/libs/api-mocks/disk.js deleted file mode 100644 index aaac051e66..0000000000 --- a/libs/api-mocks/disk.js +++ /dev/null @@ -1,140 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.disks = void 0; -var util_1 = require("@oxide/util"); -var instance_1 = require("./instance"); -var project_1 = require("./project"); -exports.disks = [ - { - id: '7f2309a5-13e3-47e0-8a4c-2a3b3bc992fd', - name: 'disk-1', - description: "it's a disk", - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'attached', instance: instance_1.instance.id }, - device_path: '/abc', - size: 2 * util_1.GiB, - block_size: 2048, - }, - { - id: '48f94570-60d8-401c-857f-5bf912d2d3fc', - name: 'disk-2', - description: "it's a second disk", - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'attached', instance: instance_1.instance.id }, - device_path: '/def', - size: 4 * util_1.GiB, - block_size: 2048, - }, - { - id: '3b768903-1d0b-4d78-9308-c12d3889bdfb', - name: 'disk-3', - description: "it's a third disk", - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'detached' }, - device_path: '/ghi', - size: 6 * util_1.GiB, - block_size: 2048, - }, - { - id: '5695b16d-e1d6-44b0-a75c-7b4299831540', - name: 'disk-4', - description: "it's a fourth disk", - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'detached' }, - device_path: '/jkl', - size: 64 * util_1.GiB, - block_size: 2048, - }, - { - id: '4d6f4c76-675f-4cda-b609-f3b8b301addb', - name: 'disk-5', - description: '', - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'detached' }, - device_path: '/jkl', - size: 128 * util_1.GiB, - block_size: 2048, - }, - { - id: '41481936-5a6b-4dcd-8dec-26c3bdc343bd', - name: 'disk-6', - description: '', - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'detached' }, - device_path: '/jkl', - size: 20 * util_1.GiB, - block_size: 2048, - }, - { - id: '704cd392-9f6b-4a2b-8410-1f1e0794db80', - name: 'disk-7', - description: '', - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'detached' }, - device_path: '/jkl', - size: 24 * util_1.GiB, - block_size: 2048, - }, - { - id: '305ee9c7-1930-4a8f-86d7-ed9eece9598e', - name: 'disk-8', - description: '', - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'detached' }, - device_path: '/jkl', - size: 16 * util_1.GiB, - block_size: 2048, - }, - { - id: 'ccad8d48-df21-4a80-8c16-683ee6bfb290', - name: 'disk-9', - description: '', - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'detached' }, - device_path: '/jkl', - size: 32 * util_1.GiB, - block_size: 2048, - }, - { - id: 'a028160f-603c-4562-bb71-d2d76f1ac2a8', - name: 'disk-10', - description: '', - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'detached' }, - device_path: '/jkl', - size: 24 * util_1.GiB, - block_size: 2048, - }, - { - id: '3f23c80f-c523-4d86-8292-2ca3f807bb12', - name: 'disk-snapshot-error', - description: '', - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - state: { state: 'detached' }, - device_path: '/jkl', - size: 12 * util_1.GiB, - block_size: 2048, - }, -]; diff --git a/libs/api-mocks/external-ip.js b/libs/api-mocks/external-ip.js deleted file mode 100644 index d0d467994d..0000000000 --- a/libs/api-mocks/external-ip.js +++ /dev/null @@ -1,37 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.externalIps = void 0; -var instance_1 = require("./instance"); -// TODO: this type represents the API response, but we need to mock more -// structure in order to be able to look up IPs for a particular instance -exports.externalIps = [ - { - instance_id: instance_1.instances[0].id, - external_ip: { - ip: "123.4.56.0", - kind: 'ephemeral', - }, - }, - // middle one has no IPs - { - instance_id: instance_1.instances[2].id, - external_ip: { - ip: "123.4.56.1", - kind: 'ephemeral', - }, - }, - { - instance_id: instance_1.instances[2].id, - external_ip: { - ip: "123.4.56.2", - kind: 'ephemeral', - }, - }, - { - instance_id: instance_1.instances[2].id, - external_ip: { - ip: "123.4.56.3", - kind: 'ephemeral', - }, - }, -]; diff --git a/libs/api-mocks/floating-ip.js b/libs/api-mocks/floating-ip.js deleted file mode 100644 index f01a30fe24..0000000000 --- a/libs/api-mocks/floating-ip.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.floatingIps = exports.floatingIp2 = exports.floatingIp = void 0; -var _1 = require("."); -// A floating IP from the default pool -exports.floatingIp = { - id: '3ca0ccb7-d66d-4fde-a871-ab9855eaea8e', - name: 'rootbeer-float', - description: 'A classic.', - instance_id: undefined, - ip: '192.168.32.1', - project_id: _1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), -}; -// A floating IP attached to a particular instance -exports.floatingIp2 = { - id: '0a00a6c3-4821-4bb8-af77-574468ac6651', - name: 'cola-float', - description: 'A favourite.', - instance_id: _1.instance.id, - ip: '192.168.64.64', - project_id: _1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), -}; -exports.floatingIps = [exports.floatingIp, exports.floatingIp2]; diff --git a/libs/api-mocks/image.js b/libs/api-mocks/image.js deleted file mode 100644 index 4251fa2313..0000000000 --- a/libs/api-mocks/image.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.images = void 0; -var util_1 = require("@oxide/util"); -var project_1 = require("./project"); -var base = { - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - block_size: 512, -}; -exports.images = [ - __assign({ id: '7ea31aad-7004-4d1e-ada6-a2e447da40b7', name: 'image-1', description: "it's an image", size: 4 * util_1.GiB, os: 'alpine', version: 'edge1', project_id: project_1.project.id }, base), - __assign({ id: '9bbba93d-aac3-4c00-ad04-2e05a555a59a', name: 'image-2', description: "it's a second image", size: 5 * util_1.GiB, os: 'alpine', version: 'edge2', project_id: project_1.project.id }, base), - __assign({ id: '4700ecf1-8f48-4ecf-b78e-816ddb76aaca', name: 'image-3', description: "it's a third image", size: 6 * util_1.GiB, os: 'alpine', version: 'edge3', project_id: project_1.project.id }, base), - __assign({ id: 'd150b87d-eb20-49d2-8b56-ff5564670e8c', name: 'image-4', description: "it's a fourth image", size: 7 * util_1.GiB, os: 'alpine', version: 'edge4', project_id: project_1.project.id }, base), - __assign({ id: 'ae46ddf5-a8d5-40fa-bcda-fcac606e3f9b', name: 'ubuntu-22-04', description: 'Latest Ubuntu LTS', os: 'ubuntu', version: '22.04', size: 1 * util_1.GiB }, base), - __assign({ id: 'a2ea1d7a-cc5a-4fda-a400-e2d2b18f53c5', name: 'ubuntu-20-04', description: 'Previous LTS', os: 'ubuntu', version: '20.04', size: 2 * util_1.GiB }, base), - __assign({ id: 'bd6aa051-8075-421d-a641-fae54a0ce8ef', name: 'arch-2022-06-01', description: 'Latest Arch Linux', os: 'arch', version: '2022.06.01', size: 3 * util_1.GiB }, base), -]; diff --git a/libs/api-mocks/index.js b/libs/api-mocks/index.js deleted file mode 100644 index cc9c24bc53..0000000000 --- a/libs/api-mocks/index.js +++ /dev/null @@ -1,50 +0,0 @@ -"use strict"; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.resetDb = exports.MSW_USER_COOKIE = exports.json = exports.handlers = void 0; -__exportStar(require("./disk"), exports); -__exportStar(require("./external-ip"), exports); -__exportStar(require("./floating-ip"), exports); -__exportStar(require("./image"), exports); -__exportStar(require("./instance"), exports); -__exportStar(require("./ip-pool"), exports); -__exportStar(require("./network-interface"), exports); -__exportStar(require("./physical-disk"), exports); -__exportStar(require("./project"), exports); -__exportStar(require("./rack"), exports); -__exportStar(require("./role-assignment"), exports); -__exportStar(require("./silo"), exports); -__exportStar(require("./sled"), exports); -__exportStar(require("./snapshot"), exports); -__exportStar(require("./sshKeys"), exports); -__exportStar(require("./user"), exports); -__exportStar(require("./user-group"), exports); -__exportStar(require("./user"), exports); -__exportStar(require("./vpc"), exports); -var handlers_1 = require("./msw/handlers"); -Object.defineProperty(exports, "handlers", { enumerable: true, get: function () { return handlers_1.handlers; } }); -var util_1 = require("./msw/util"); -Object.defineProperty(exports, "json", { enumerable: true, get: function () { return util_1.json; } }); -Object.defineProperty(exports, "MSW_USER_COOKIE", { enumerable: true, get: function () { return util_1.MSW_USER_COOKIE; } }); -var db_1 = require("./msw/db"); -Object.defineProperty(exports, "resetDb", { enumerable: true, get: function () { return db_1.resetDb; } }); diff --git a/libs/api-mocks/instance.js b/libs/api-mocks/instance.js deleted file mode 100644 index 9b2800f4c3..0000000000 --- a/libs/api-mocks/instance.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.instances = exports.instance = void 0; -var project_1 = require("./project"); -exports.instance = { - id: '935499b3-fd96-432a-9c21-83a3dc1eece4', - name: 'db1', - ncpus: 7, - memory: 1024 * 1024 * 256, - description: 'an instance', - hostname: 'oxide.com', - project_id: project_1.project.id, - run_state: 'running', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - time_run_state_updated: new Date().toISOString(), -}; -var failedInstance = { - id: 'b5946edc-5bed-4597-88ab-9a8beb9d32a4', - name: 'you-fail', - ncpus: 7, - memory: 1024 * 1024 * 256, - description: 'a failed instance', - hostname: 'oxide.com', - project_id: project_1.project.id, - run_state: 'failed', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - time_run_state_updated: new Date().toISOString(), -}; -var startingInstance = { - id: '16737f54-1f76-4c96-8b7c-9d24971c1d62', - name: 'not-there-yet', - ncpus: 7, - memory: 1024 * 1024 * 256, - description: 'a starting instance', - hostname: 'oxide.com', - project_id: project_1.project.id, - run_state: 'starting', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - time_run_state_updated: new Date().toISOString(), -}; -exports.instances = [exports.instance, failedInstance, startingInstance]; diff --git a/libs/api-mocks/ip-pool.js b/libs/api-mocks/ip-pool.js deleted file mode 100644 index d00f3261af..0000000000 --- a/libs/api-mocks/ip-pool.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ipPoolRanges = exports.ipPoolSilos = exports.ipPools = void 0; -var silo_1 = require("./silo"); -var ipPool1 = { - id: '69b5c583-74a9-451a-823d-0741c1ec66e2', - name: 'ip-pool-1', - description: '', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), -}; -var ipPool2 = { - id: 'af2fbe06-b21d-4364-96b7-a58220bc3242', - name: 'ip-pool-2', - description: '', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), -}; -var ipPool3 = { - id: '8929a9ec-03d7-4027-8bf3-dda76627de07', - name: 'ip-pool-3', - description: '', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), -}; -exports.ipPools = [ipPool1, ipPool2, ipPool3]; -exports.ipPoolSilos = [ - { - ip_pool_id: ipPool1.id, - silo_id: silo_1.defaultSilo.id, - is_default: true, - }, - { - ip_pool_id: ipPool2.id, - silo_id: silo_1.defaultSilo.id, - is_default: false, - }, -]; -exports.ipPoolRanges = [ - { - id: 'bbfcf3f2-061e-4334-a0e7-dfcd8171f87e', - ip_pool_id: ipPool1.id, - range: { - first: '10.0.0.1', - last: '10.0.0.5', - }, - time_created: new Date().toISOString(), - }, - { - id: 'df05795b-cb88-4971-9865-ac2995c2b2d4', - ip_pool_id: ipPool1.id, - range: { - first: '10.0.0.20', - last: '10.0.0.22', - }, - time_created: new Date().toISOString(), - }, - { - id: '7e6e94b9-748e-4219-83a3-cec76253ec70', - ip_pool_id: ipPool2.id, - range: { - first: '10.0.0.33', - last: '10.0.0.38', - }, - time_created: new Date().toISOString(), - }, -]; diff --git a/libs/api-mocks/json-type.js b/libs/api-mocks/json-type.js deleted file mode 100644 index a764bbeff0..0000000000 --- a/libs/api-mocks/json-type.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Json = void 0; -var msw_handlers_1 = require("@oxide/gen/msw-handlers"); -Object.defineProperty(exports, "Json", { enumerable: true, get: function () { return msw_handlers_1.Json; } }); diff --git a/libs/api-mocks/metrics.js b/libs/api-mocks/metrics.js deleted file mode 100644 index d2bc3c6921..0000000000 --- a/libs/api-mocks/metrics.js +++ /dev/null @@ -1,37 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.genI64Data = exports.genCumulativeI64Data = void 0; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -var date_fns_1 = require("date-fns"); -/** evenly distribute the `values` across the time interval */ -var genCumulativeI64Data = function (values, startTime, endTime) { - var intervalSeconds = (0, date_fns_1.differenceInSeconds)(endTime, startTime) / values.length; - return values.map(function (value, i) { return ({ - datum: { - datum: { - value: value, - start_time: startTime.toISOString(), - }, - type: 'cumulative_i64', - }, - timestamp: (0, date_fns_1.addSeconds)(startTime, i * intervalSeconds).toISOString(), - }); }); -}; -exports.genCumulativeI64Data = genCumulativeI64Data; -var genI64Data = function (values, startTime, endTime) { - var intervalSeconds = (0, date_fns_1.differenceInSeconds)(endTime, startTime) / values.length; - return values.map(function (value, i) { return ({ - datum: { - datum: value, - type: 'i64', - }, - timestamp: (0, date_fns_1.addSeconds)(startTime, i * intervalSeconds).toISOString(), - }); }); -}; -exports.genI64Data = genI64Data; diff --git a/libs/api-mocks/msw/db.js b/libs/api-mocks/msw/db.js deleted file mode 100644 index bf71c217b3..0000000000 --- a/libs/api-mocks/msw/db.js +++ /dev/null @@ -1,307 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -var __rest = (this && this.__rest) || function (s, e) { - var t = {}; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) - t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) - t[p[i]] = s[p[i]]; - } - return t; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.resetDb = exports.db = exports.utilizationForSilo = exports.lookup = exports.lookupById = exports.notFoundErr = void 0; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -// note that isUuid checks for any kind of UUID. strictly speaking, we should -// only be checking for v4 -var uuid_1 = require("uuid"); -var mock = require("@oxide/api-mocks"); -var api_mocks_1 = require("@oxide/api-mocks"); -var util_1 = require("@oxide/util"); -var util_2 = require("./util"); -var notFoundBody = { error_code: 'ObjectNotFound' }; -var notFoundErr = function (msg) { - var message = msg ? "not found: ".concat(msg) : 'not found'; - return (0, util_2.json)({ error_code: 'ObjectNotFound', message: message }, { status: 404 }); -}; -exports.notFoundErr = notFoundErr; -var lookupById = function (table, id) { - var item = table.find(function (i) { return i.id === id; }); - if (!item) - throw exports.notFoundErr; - return item; -}; -exports.lookupById = lookupById; -exports.lookup = { - project: function (_a) { - var id = _a.project; - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.projects, id); - var project = exports.db.projects.find(function (p) { return p.name === id; }); - if (!project) - throw exports.notFoundErr; - return project; - }, - instance: function (_a) { - var id = _a.instance, projectSelector = __rest(_a, ["instance"]); - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.instances, id); - var project = exports.lookup.project(projectSelector); - var instance = exports.db.instances.find(function (i) { return i.project_id === project.id && i.name === id; }); - if (!instance) - throw exports.notFoundErr; - return instance; - }, - networkInterface: function (_a) { - var id = _a.interface, instanceSelector = __rest(_a, ["interface"]); - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.networkInterfaces, id); - var instance = exports.lookup.instance(instanceSelector); - var nic = exports.db.networkInterfaces.find(function (n) { return n.instance_id === instance.id && n.name === id; }); - if (!nic) - throw exports.notFoundErr; - return nic; - }, - disk: function (_a) { - var id = _a.disk, projectSelector = __rest(_a, ["disk"]); - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.disks, id); - var project = exports.lookup.project(projectSelector); - var disk = exports.db.disks.find(function (d) { return d.project_id === project.id && d.name === id; }); - if (!disk) - throw exports.notFoundErr; - return disk; - }, - floatingIp: function (_a) { - var id = _a.floatingIp, projectSelector = __rest(_a, ["floatingIp"]); - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.floatingIps, id); - var project = exports.lookup.project(projectSelector); - var floatingIp = exports.db.floatingIps.find(function (i) { return i.project_id === project.id && i.name === id; }); - if (!floatingIp) - throw exports.notFoundErr; - return floatingIp; - }, - snapshot: function (_a) { - var id = _a.snapshot, projectSelector = __rest(_a, ["snapshot"]); - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.snapshots, id); - var project = exports.lookup.project(projectSelector); - var snapshot = exports.db.snapshots.find(function (i) { return i.project_id === project.id && i.name === id; }); - if (!snapshot) - throw exports.notFoundErr; - return snapshot; - }, - vpc: function (_a) { - var id = _a.vpc, projectSelector = __rest(_a, ["vpc"]); - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.vpcs, id); - var project = exports.lookup.project(projectSelector); - var vpc = exports.db.vpcs.find(function (v) { return v.project_id === project.id && v.name === id; }); - if (!vpc) - throw exports.notFoundErr; - return vpc; - }, - vpcSubnet: function (_a) { - var id = _a.subnet, vpcSelector = __rest(_a, ["subnet"]); - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.vpcSubnets, id); - var vpc = exports.lookup.vpc(vpcSelector); - var subnet = exports.db.vpcSubnets.find(function (s) { return s.vpc_id === vpc.id && s.name === id; }); - if (!subnet) - throw exports.notFoundErr; - return subnet; - }, - image: function (_a) { - var id = _a.image, projectId = _a.project; - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.images, id); - var image; - if (projectId === undefined) { - // silo image - image = exports.db.images.find(function (d) { return d.project_id === undefined && d.name === id; }); - } - else { - // project image - var project_1 = exports.lookup.project({ project: projectId }); - image = exports.db.images.find(function (d) { return d.project_id === project_1.id && d.name === id; }); - } - if (!image) - throw exports.notFoundErr; - return image; - }, - ipPool: function (_a) { - var id = _a.pool; - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.ipPools, id); - var pool = exports.db.ipPools.find(function (p) { return p.name === id; }); - if (!pool) - throw exports.notFoundErr; - return pool; - }, - // unusual one because it's a sibling relationship. we look up both the pool and the silo first - ipPoolSiloLink: function (_a) { - var poolId = _a.pool, siloId = _a.silo; - var pool = exports.lookup.ipPool({ pool: poolId }); - var silo = exports.lookup.silo({ silo: siloId }); - var ipPoolSilo = exports.db.ipPoolSilos.find(function (ips) { return ips.ip_pool_id === pool.id && ips.silo_id === silo.id; }); - if (!ipPoolSilo) - throw exports.notFoundErr; - return ipPoolSilo; - }, - // unusual because it returns a list, but we need it for multiple endpoints - siloIpPools: function (path) { - var silo = exports.lookup.silo(path); - // effectively join db.ipPools and db.ipPoolSilos on ip_pool_id - return exports.db.ipPoolSilos - .filter(function (link) { return link.silo_id === silo.id; }) - .map(function (link) { - var pool = exports.db.ipPools.find(function (pool) { return pool.id === link.ip_pool_id; }); - // this should never happen - if (!pool) { - var linkStr = JSON.stringify(link); - var message = "Found IP pool-silo link without corresponding pool: ".concat(linkStr); - throw (0, util_2.json)({ message: message }, { status: 500 }); - } - return __assign(__assign({}, pool), { is_default: link.is_default }); - }); - }, - samlIdp: function (_a) { - var id = _a.provider, siloSelector = __rest(_a, ["provider"]); - if (!id) - throw exports.notFoundErr; - var silo = exports.lookup.silo(siloSelector); - var dbIdp = exports.db.identityProviders.find(function (_a) { - var type = _a.type, siloId = _a.siloId, provider = _a.provider; - return type === 'saml' && siloId === silo.id && provider.name === id; - }); - if (!dbIdp) - throw exports.notFoundErr; - return dbIdp.provider; - }, - silo: function (_a) { - var id = _a.silo; - if (!id) - throw exports.notFoundErr; - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(exports.db.silos, id); - var silo = exports.db.silos.find(function (o) { return o.name === id; }); - if (!silo) - throw exports.notFoundErr; - return silo; - }, - sled: function (_a) { - var id = _a.sledId; - if (!id) - throw exports.notFoundErr; - return (0, exports.lookupById)(exports.db.sleds, id); - }, - sshKey: function (_a) { - var id = _a.sshKey; - // we don't have a concept of mock session. assume the user is user1 - var userSshKeys = exports.db.sshKeys.filter(function (key) { return key.silo_user_id === api_mocks_1.user1.id; }); - if ((0, uuid_1.validate)(id)) - return (0, exports.lookupById)(userSshKeys, id); - var sshKey = userSshKeys.find(function (key) { return key.name === id; }); - if (!sshKey) - throw exports.notFoundErr; - return sshKey; - }, -}; -function utilizationForSilo(silo) { - var quotas = exports.db.siloQuotas.find(function (q) { return q.silo_id === silo.id; }); - if (!quotas) - throw (0, util_2.internalError)(); - var provisioned = exports.db.siloProvisioned.find(function (p) { return p.silo_id === silo.id; }); - if (!provisioned) - throw (0, util_2.internalError)(); - return { - allocated: (0, util_1.pick)(quotas, 'cpus', 'storage', 'memory'), - provisioned: (0, util_1.pick)(provisioned, 'cpus', 'storage', 'memory'), - silo_id: silo.id, - silo_name: silo.name, - }; -} -exports.utilizationForSilo = utilizationForSilo; -var initDb = { - disks: __spreadArray([], mock.disks, true), - diskBulkImportState: new Map(), - floatingIps: __spreadArray([], mock.floatingIps, true), - userGroups: __spreadArray([], mock.userGroups, true), - /** Join table for `users` and `userGroups` */ - groupMemberships: __spreadArray([], mock.groupMemberships, true), - images: __spreadArray([], mock.images, true), - externalIps: __spreadArray([], mock.externalIps, true), - instances: __spreadArray([], mock.instances, true), - ipPools: __spreadArray([], mock.ipPools, true), - ipPoolSilos: __spreadArray([], mock.ipPoolSilos, true), - ipPoolRanges: __spreadArray([], mock.ipPoolRanges, true), - networkInterfaces: [mock.networkInterface], - physicalDisks: __spreadArray([], mock.physicalDisks, true), - projects: __spreadArray([], mock.projects, true), - racks: __spreadArray([], mock.racks, true), - roleAssignments: __spreadArray([], mock.roleAssignments, true), - silos: __spreadArray([], mock.silos, true), - siloQuotas: __spreadArray([], mock.siloQuotas, true), - siloProvisioned: __spreadArray([], mock.siloProvisioned, true), - identityProviders: __spreadArray([], mock.identityProviders, true), - sleds: __spreadArray([], mock.sleds, true), - snapshots: __spreadArray([], mock.snapshots, true), - sshKeys: __spreadArray([], mock.sshKeys, true), - users: __spreadArray([], mock.users, true), - vpcFirewallRules: __spreadArray([], mock.defaultFirewallRules, true), - vpcs: [mock.vpc], - vpcSubnets: [mock.vpcSubnet], -}; -exports.db = structuredClone(initDb); -function resetDb() { - exports.db = structuredClone(initDb); -} -exports.resetDb = resetDb; diff --git a/libs/api-mocks/msw/handlers.js b/libs/api-mocks/msw/handlers.js deleted file mode 100644 index 2eb0a18f58..0000000000 --- a/libs/api-mocks/msw/handlers.js +++ /dev/null @@ -1,1107 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.handlers = void 0; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -var msw_1 = require("msw"); -var uuid_1 = require("uuid"); -var api_1 = require("@oxide/api"); -var msw_handlers_1 = require("@oxide/gen/msw-handlers"); -var util_1 = require("@oxide/util"); -var metrics_1 = require("../metrics"); -var serial_1 = require("../serial"); -var silo_1 = require("../silo"); -var db_1 = require("./db"); -var util_2 = require("./util"); -// Note the *JSON types. Those represent actual API request and response bodies, -// the snake-cased objects coming straight from the API before the generated -// client camel-cases the keys and parses date fields. Inside the mock API everything -// is *JSON type. -exports.handlers = (0, msw_handlers_1.makeHandlers)({ - ping: function () { return ({ status: 'ok' }); }, - deviceAuthRequest: function () { return 200; }, - deviceAuthConfirm: function (_a) { - var body = _a.body; - return (body.user_code === 'ERRO-RABC' ? 400 : 200); - }, - deviceAccessToken: function () { return 200; }, - loginLocal: function (_a) { - var password = _a.body.password; - return (password === 'bad' ? 401 : 200); - }, - groupList: function (params) { return (0, util_2.paginated)(params.query, db_1.db.userGroups); }, - groupView: function (params) { return (0, db_1.lookupById)(db_1.db.userGroups, params.path.groupId); }, - projectList: function (params) { return (0, util_2.paginated)(params.query, db_1.db.projects); }, - projectCreate: function (_a) { - var body = _a.body; - (0, util_2.errIfExists)(db_1.db.projects, { name: body.name }, 'project'); - var newProject = __assign(__assign({ id: (0, uuid_1.v4)() }, body), (0, util_2.getTimestamps)()); - db_1.db.projects.push(newProject); - return (0, msw_handlers_1.json)(newProject, { status: 201 }); - }, - projectView: function (_a) { - var path = _a.path; - if (path.project.endsWith('error-503')) { - throw util_2.unavailableErr; - } - return db_1.lookup.project(__assign({}, path)); - }, - projectUpdate: function (_a) { - var body = _a.body, path = _a.path; - var project = db_1.lookup.project(__assign({}, path)); - if (body.name) { - // only check for existing name if it's being changed - if (body.name !== project.name) { - (0, util_2.errIfExists)(db_1.db.projects, { name: body.name }); - } - project.name = body.name; - } - project.description = body.description || ''; - return project; - }, - projectDelete: function (_a) { - var path = _a.path; - var project = db_1.lookup.project(__assign({}, path)); - // imitate API logic (TODO: check for every other kind of project child) - if (db_1.db.vpcs.some(function (vpc) { return vpc.project_id === project.id; })) { - throw 'Project to be deleted contains a VPC'; - } - db_1.db.projects = db_1.db.projects.filter(function (p) { return p.id !== project.id; }); - return 204; - }, - diskList: function (_a) { - var query = _a.query; - var project = db_1.lookup.project(query); - var disks = db_1.db.disks.filter(function (d) { return d.project_id === project.id; }); - return (0, util_2.paginated)(query, disks); - }, - diskCreate: function (_a) { - var body = _a.body, query = _a.query; - var project = db_1.lookup.project(query); - (0, util_2.errIfExists)(db_1.db.disks, { name: body.name, project_id: project.id }); - if (body.name === 'disk-create-500') - throw 500; - var name = body.name, description = body.description, size = body.size, disk_source = body.disk_source; - var newDisk = __assign({ id: (0, uuid_1.v4)(), project_id: project.id, state: disk_source.type === 'importing_blocks' - ? { state: 'import_ready' } - : { state: 'creating' }, device_path: '/mnt/disk', name: name, description: description, size: size, - // TODO: for non-blank disk sources, look up image or snapshot by ID and - // pull block size from there - block_size: disk_source.type === 'blank' ? disk_source.block_size : 512 }, (0, util_2.getTimestamps)()); - db_1.db.disks.push(newDisk); - return (0, msw_handlers_1.json)(newDisk, { status: 201 }); - }, - diskView: function (_a) { - var path = _a.path, query = _a.query; - return db_1.lookup.disk(__assign(__assign({}, path), query)); - }, - diskDelete: function (_a) { - var path = _a.path, query = _a.query; - var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); - if (!api_1.diskCan.delete(disk)) { - throw 'Cannot delete disk in state ' + disk.state.state; - } - db_1.db.disks = db_1.db.disks.filter(function (d) { return d.id !== disk.id; }); - return 204; - }, - diskMetricsList: function (_a) { - var path = _a.path, query = _a.query; - db_1.lookup.disk(__assign(__assign({}, path), query)); - var _b = (0, util_2.getStartAndEndTime)(query), startTime = _b.startTime, endTime = _b.endTime; - if (endTime <= startTime) - return { items: [] }; - return { - items: (0, metrics_1.genCumulativeI64Data)(new Array(1000).fill(0).map(function (_x, i) { return Math.floor(Math.tanh(i / 500) * 3000); }), startTime, endTime), - }; - }, - diskBulkWriteImportStart: function (_a) { - var path = _a.path, query = _a.query; - var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); - if (disk.name === 'import-start-500') - throw 500; - if (disk.state.state !== 'import_ready') { - throw 'Can only enter state importing_from_bulk_write from import_ready'; - } - // throw 400 - db_1.db.diskBulkImportState.set(disk.id, { blocks: {} }); - disk.state = { state: 'importing_from_bulk_writes' }; - return 204; - }, - diskBulkWriteImportStop: function (_a) { - var path = _a.path, query = _a.query; - var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); - if (disk.name === 'import-stop-500') - throw 500; - if (disk.state.state !== 'importing_from_bulk_writes') { - throw 'Can only stop import for disk in state importing_from_bulk_write'; - } - db_1.db.diskBulkImportState.delete(disk.id); - disk.state = { state: 'import_ready' }; - return 204; - }, - diskBulkWriteImport: function (_a) { - var path = _a.path, query = _a.query, body = _a.body; - var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); - var diskImport = db_1.db.diskBulkImportState.get(disk.id); - if (!diskImport) - throw db_1.notFoundErr; - // if (Math.random() < 0.01) throw 400 - diskImport.blocks[body.offset] = true; - return 204; - }, - diskFinalizeImport: function (_a) { - var path = _a.path, query = _a.query, body = _a.body; - var disk = db_1.lookup.disk(__assign(__assign({}, path), query)); - if (disk.name === 'disk-finalize-500') - throw 500; - if (disk.state.state !== 'import_ready') { - throw "Cannot finalize disk in state ".concat(disk.state.state, ". Must be import_ready."); - } - // for now, don't check that the file is complete. the API doesn't - disk.state = { state: 'detached' }; - if (body.snapshot_name) { - var newSnapshot = __assign(__assign({ id: (0, uuid_1.v4)(), name: body.snapshot_name, description: 'temporary snapshot for making an image' }, (0, util_2.getTimestamps)()), { state: 'ready', project_id: disk.project_id, disk_id: disk.id, size: disk.size }); - db_1.db.snapshots.push(newSnapshot); - } - return 204; - }, - // floatingIpCreate({ body, query }) { - // const project = lookup.project(query) - // errIfExists(db.floatingIps, { name: body.name }) - // const newFloatingIp: Json = { - // id: uuid(), - // project_id: project.id, - // ip: `${[...Array(4)].map(()=>Math.floor(Math.random()*256)).join('.')}`, - // ...body, - // ...getTimestamps(), - // } - // db.floatingIps.push(newFloatingIp) - // return json(newFloatingIp, { status: 201 }) - // }, - // floatingIpList({ query }) { - // const project = lookup.project(query) - // const ips = db.floatingIps.filter((i) => i.project_id === project.id) - // return paginated(query, ips) - // }, - // floatingIpView({ query, path }) { - // const project = lookup.project(query) - // const ip = db.floatingIps.filter( - // (i) => i.project_id === project.id && i.name === path.floatingIp - // )[0] - // return ip - // }, - // floatingIpDelete({ path, query }) { - // const floatingIp = lookup.floatingIp({ ...path, ...query }) - // db.floatingIps = db.floatingIps.filter((i) => i.id !== floatingIp.id) - // return 204 - // }, - // floatingIpAttach({ body, path, query }) { - // const floatingIp = lookup.floatingIp({ ...path, ...query }) - // const instance = lookup.instance({ instance: body.parent }) - // console.log(instance) - // floatingIp.instance_id = instance.id - // console.log(floatingIp) - // return floatingIp - // }, - // floatingIpDetach({ path, query }) { - // const floatingIp: FloatingIp = lookup.floatingIp({ ...path, ...query }) - // db.floatingIps = db.floatingIps.map((ip : FloatingIp) => (ip.id !== floatingIp.id) ? ip : { ...ip, instance_id: undefined }) - // return 204 - // }, - imageList: function (_a) { - var query = _a.query; - if (query.project) { - var project_1 = db_1.lookup.project(query); - var images_1 = db_1.db.images.filter(function (i) { return i.project_id === project_1.id; }); - return (0, util_2.paginated)(query, images_1); - } - // silo images - var images = db_1.db.images.filter(function (i) { return !i.project_id; }); - return (0, util_2.paginated)(query, images); - }, - imageCreate: function (_a) { - var body = _a.body, query = _a.query; - var project_id = undefined; - if (query.project) { - project_id = db_1.lookup.project(query).id; - } - (0, util_2.errIfExists)(db_1.db.images, { name: body.name, project_id: project_id }); - var size = body.source.type === 'snapshot' - ? db_1.lookup.snapshot({ snapshot: body.source.id }).size - : 100; - var newImage = __assign(__assign({ id: (0, uuid_1.v4)(), project_id: project_id, size: size, block_size: 512 }, body), (0, util_2.getTimestamps)()); - db_1.db.images.push(newImage); - return (0, msw_handlers_1.json)(newImage, { status: 201 }); - }, - imageView: function (_a) { - var path = _a.path, query = _a.query; - return db_1.lookup.image(__assign(__assign({}, path), query)); - }, - imageDelete: function (_a) { - var path = _a.path, query = _a.query, cookies = _a.cookies; - // if it's a silo image, you need silo write to delete it - if (!query.project) { - (0, util_2.requireRole)(cookies, 'silo', silo_1.defaultSilo.id, 'collaborator'); - } - var image = db_1.lookup.image(__assign(__assign({}, path), query)); - db_1.db.images = db_1.db.images.filter(function (i) { return i.id !== image.id; }); - return 204; - }, - imagePromote: function (_a) { - var path = _a.path, query = _a.query; - var image = db_1.lookup.image(__assign(__assign({}, path), query)); - delete image.project_id; - return (0, msw_handlers_1.json)(image, { status: 202 }); - }, - imageDemote: function (_a) { - var path = _a.path, query = _a.query; - var image = db_1.lookup.image(__assign(__assign({}, path), query)); - var project = db_1.lookup.project(__assign(__assign({}, path), query)); - image.project_id = project.id; - return (0, msw_handlers_1.json)(image, { status: 202 }); - }, - instanceList: function (_a) { - var query = _a.query; - var project = db_1.lookup.project(query); - var instances = db_1.db.instances.filter(function (i) { return i.project_id === project.id; }); - return (0, util_2.paginated)(query, instances); - }, - instanceCreate: function (_a) { - var _b, _c, _d; - var body = _a.body, query = _a.query; - return __awaiter(this, void 0, void 0, function () { - var project, instanceId, _i, _e, diskParams, _f, _g, diskParams, size, name_1, description, disk_source, newDisk, disk, anyVpc, anySubnet, newInstance; - return __generator(this, function (_h) { - project = db_1.lookup.project(query); - if (body.name === 'no-default-pool') { - throw (0, db_1.notFoundErr)('default IP pool for current silo'); - } - (0, util_2.errIfExists)(db_1.db.instances, { name: body.name, project_id: project.id }, 'instance'); - instanceId = (0, uuid_1.v4)(); - // TODO: These values should ultimately be represented in the schema and - // checked with the generated schema validation code. - if (body.memory > api_1.INSTANCE_MAX_RAM_GiB * util_1.GiB) { - throw "Memory must be less than ".concat(api_1.INSTANCE_MAX_RAM_GiB, " GiB"); - } - if (body.memory < api_1.INSTANCE_MIN_RAM_GiB * util_1.GiB) { - throw "Memory must be at least ".concat(api_1.INSTANCE_MIN_RAM_GiB, " GiB"); - } - if (body.ncpus > api_1.INSTANCE_MAX_CPU) { - throw "vCPUs must be less than ".concat(api_1.INSTANCE_MAX_CPU); - } - if (body.ncpus < 1) { - throw "Must have at least 1 vCPU"; - } - /** - * Eagerly check for disk errors. Execution will stop early and prevent orphaned disks from - * being created if there's a failure. In omicron this is done automatically via an undo on the saga. - */ - for (_i = 0, _e = body.disks || []; _i < _e.length; _i++) { - diskParams = _e[_i]; - if (diskParams.type === 'create') { - (0, util_2.errIfExists)(db_1.db.disks, { name: diskParams.name, project_id: project.id }, 'disk'); - (0, util_2.errIfInvalidDiskSize)(diskParams); - } - else { - db_1.lookup.disk(__assign(__assign({}, query), { disk: diskParams.name })); - } - } - /** - * Eagerly check for nic lookup failures. Execution will stop early and prevent orphaned nics from - * being created if there's a failure. In omicron this is done automatically via an undo on the saga. - */ - if (((_b = body.network_interfaces) === null || _b === void 0 ? void 0 : _b.type) === 'create') { - if (body.network_interfaces.params.length > api_1.MAX_NICS_PER_INSTANCE) { - throw "Cannot create more than ".concat(api_1.MAX_NICS_PER_INSTANCE, " nics per instance"); - } - body.network_interfaces.params.forEach(function (_a) { - var vpc_name = _a.vpc_name, subnet_name = _a.subnet_name; - db_1.lookup.vpc(__assign(__assign({}, query), { vpc: vpc_name })); - db_1.lookup.vpcSubnet(__assign(__assign({}, query), { vpc: vpc_name, subnet: subnet_name })); - }); - } - for (_f = 0, _g = body.disks || []; _f < _g.length; _f++) { - diskParams = _g[_f]; - if (diskParams.type === 'create') { - size = diskParams.size, name_1 = diskParams.name, description = diskParams.description, disk_source = diskParams.disk_source; - newDisk = __assign({ id: (0, uuid_1.v4)(), name: name_1, description: description, size: size, project_id: project.id, state: { state: 'attached', instance: instanceId }, device_path: '/mnt/disk', block_size: disk_source.type === 'blank' ? disk_source.block_size : 4096 }, (0, util_2.getTimestamps)()); - db_1.db.disks.push(newDisk); - } - else { - disk = db_1.lookup.disk(__assign(__assign({}, query), { disk: diskParams.name })); - disk.state = { state: 'attached', instance: instanceId }; - } - } - anyVpc = db_1.db.vpcs.find(function (v) { return v.project_id === project.id; }); - anySubnet = db_1.db.vpcSubnets.find(function (s) { return s.vpc_id === (anyVpc === null || anyVpc === void 0 ? void 0 : anyVpc.id); }); - if (((_c = body.network_interfaces) === null || _c === void 0 ? void 0 : _c.type) === 'default' && anyVpc && anySubnet) { - db_1.db.networkInterfaces.push(__assign({ id: (0, uuid_1.v4)(), description: 'The default network interface', instance_id: instanceId, primary: true, mac: '00:00:00:00:00:00', ip: '127.0.0.1', name: 'default', vpc_id: anyVpc.id, subnet_id: anySubnet.id }, (0, util_2.getTimestamps)())); - } - else if (((_d = body.network_interfaces) === null || _d === void 0 ? void 0 : _d.type) === 'create') { - body.network_interfaces.params.forEach(function (_a, i) { - var name = _a.name, description = _a.description, ip = _a.ip, subnet_name = _a.subnet_name, vpc_name = _a.vpc_name; - db_1.db.networkInterfaces.push(__assign({ id: (0, uuid_1.v4)(), name: name, description: description, instance_id: instanceId, primary: i === 0 ? true : false, mac: '00:00:00:00:00:00', ip: ip || '127.0.0.1', vpc_id: db_1.lookup.vpc(__assign(__assign({}, query), { vpc: vpc_name })).id, subnet_id: db_1.lookup.vpcSubnet(__assign(__assign({}, query), { vpc: vpc_name, subnet: subnet_name })) - .id }, (0, util_2.getTimestamps)())); - }); - } - newInstance = __assign(__assign(__assign({ id: instanceId, project_id: project.id }, (0, util_1.pick)(body, 'name', 'description', 'hostname', 'memory', 'ncpus')), (0, util_2.getTimestamps)()), { run_state: 'running', time_run_state_updated: new Date().toISOString() }); - db_1.db.instances.push(newInstance); - return [2 /*return*/, (0, msw_handlers_1.json)(newInstance, { status: 201 })]; - }); - }); - }, - instanceView: function (_a) { - var path = _a.path, query = _a.query; - return db_1.lookup.instance(__assign(__assign({}, path), query)); - }, - instanceDelete: function (_a) { - var path = _a.path, query = _a.query; - var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); - db_1.db.instances = db_1.db.instances.filter(function (i) { return i.id !== instance.id; }); - return 204; - }, - instanceDiskList: function (_a) { - var path = _a.path, query = _a.query; - var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); - // TODO: Should disk instance state be `instance_id` instead of `instance`? - var disks = db_1.db.disks.filter(function (d) { return 'instance' in d.state && d.state.instance === instance.id; }); - return (0, util_2.paginated)(query, disks); - }, - instanceDiskAttach: function (_a) { - var body = _a.body, path = _a.path, projectParams = _a.query; - var instance = db_1.lookup.instance(__assign(__assign({}, path), projectParams)); - if (instance.run_state !== 'stopped') { - throw 'Cannot attach disk to instance that is not stopped'; - } - var disk = db_1.lookup.disk(__assign(__assign({}, projectParams), { disk: body.disk })); - disk.state = { - state: 'attached', - instance: instance.id, - }; - return disk; - }, - instanceDiskDetach: function (_a) { - var body = _a.body, path = _a.path, projectParams = _a.query; - var instance = db_1.lookup.instance(__assign(__assign({}, path), projectParams)); - if (instance.run_state !== 'stopped') { - throw 'Cannot detach disk from instance that is not stopped'; - } - var disk = db_1.lookup.disk(__assign(__assign({}, projectParams), { disk: body.disk })); - disk.state = { state: 'detached' }; - return disk; - }, - instanceExternalIpList: function (_a) { - var path = _a.path, query = _a.query; - var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); - var externalIps = db_1.db.externalIps - .filter(function (eip) { return eip.instance_id === instance.id; }) - .map(function (eip) { return eip.external_ip; }); - // endpoint is not paginated. or rather, it's fake paginated - return { items: externalIps }; - }, - instanceNetworkInterfaceList: function (_a) { - var query = _a.query; - var instance = db_1.lookup.instance(query); - var nics = db_1.db.networkInterfaces.filter(function (n) { return n.instance_id === instance.id; }); - return (0, util_2.paginated)(query, nics); - }, - instanceNetworkInterfaceCreate: function (_a) { - var body = _a.body, query = _a.query; - var instance = db_1.lookup.instance(query); - var nicsForInstance = db_1.db.networkInterfaces.filter(function (n) { return n.instance_id === instance.id; }); - (0, util_2.errIfExists)(nicsForInstance, { name: body.name }); - var name = body.name, description = body.description, subnet_name = body.subnet_name, vpc_name = body.vpc_name, ip = body.ip; - var vpc = db_1.lookup.vpc(__assign(__assign({}, query), { vpc: vpc_name })); - var subnet = db_1.lookup.vpcSubnet(__assign(__assign({}, query), { vpc: vpc_name, subnet: subnet_name })); - var newNic = __assign({ id: (0, uuid_1.v4)(), - // matches API logic: https://github.com/oxidecomputer/omicron/blob/ae22982/nexus/src/db/queries/network_interface.rs#L982-L1015 - primary: nicsForInstance.length === 0, instance_id: instance.id, name: name, description: description, ip: ip || '123.45.68.8', vpc_id: vpc.id, subnet_id: subnet.id, mac: '' }, (0, util_2.getTimestamps)()); - db_1.db.networkInterfaces.push(newNic); - return newNic; - }, - instanceNetworkInterfaceView: function (_a) { - var path = _a.path, query = _a.query; - return db_1.lookup.networkInterface(__assign(__assign({}, path), query)); - }, - instanceNetworkInterfaceUpdate: function (_a) { - var body = _a.body, path = _a.path, query = _a.query; - var nic = db_1.lookup.networkInterface(__assign(__assign({}, path), query)); - if (body.name) { - nic.name = body.name; - } - if (typeof body.description === 'string') { - nic.description = body.description; - } - if (typeof body.primary === 'boolean' && body.primary !== nic.primary) { - if (nic.primary) { - throw 'Cannot remove the primary interface'; - } - db_1.db.networkInterfaces - .filter(function (n) { return n.instance_id === nic.instance_id; }) - .forEach(function (n) { - n.primary = false; - }); - nic.primary = !!body.primary; - } - return nic; - }, - instanceNetworkInterfaceDelete: function (_a) { - var path = _a.path, query = _a.query; - var nic = db_1.lookup.networkInterface(__assign(__assign({}, path), query)); - db_1.db.networkInterfaces = db_1.db.networkInterfaces.filter(function (n) { return n.id !== nic.id; }); - return 204; - }, - instanceReboot: function (_a) { - var path = _a.path, query = _a.query; - var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); - instance.run_state = 'rebooting'; - setTimeout(function () { - instance.run_state = 'running'; - }, 3000); - return (0, msw_handlers_1.json)(instance, { status: 202 }); - }, - instanceSerialConsole: function (_params) { - return __awaiter(this, void 0, void 0, function () { - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, (0, msw_1.delay)(3000)]; - case 1: - _a.sent(); - return [2 /*return*/, (0, msw_handlers_1.json)(serial_1.serial)]; - } - }); - }); - }, - instanceStart: function (_a) { - var path = _a.path, query = _a.query; - var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); - instance.run_state = 'running'; - return (0, msw_handlers_1.json)(instance, { status: 202 }); - }, - instanceStop: function (_a) { - var path = _a.path, query = _a.query; - var instance = db_1.lookup.instance(__assign(__assign({}, path), query)); - instance.run_state = 'stopped'; - return (0, msw_handlers_1.json)(instance, { status: 202 }); - }, - ipPoolList: function (_a) { - var query = _a.query; - return (0, util_2.paginated)(query, db_1.db.ipPools); - }, - siloIpPoolList: function (_a) { - var path = _a.path, query = _a.query; - var pools = db_1.lookup.siloIpPools(path); - return (0, util_2.paginated)(query, pools); - }, - projectIpPoolList: function (_a) { - var query = _a.query; - var pools = db_1.lookup.siloIpPools({ silo: silo_1.defaultSilo.id }); - return (0, util_2.paginated)(query, pools); - }, - projectIpPoolView: function (_a) { - var path = _a.path; - // this will 404 if it doesn't exist at all... - var pool = db_1.lookup.ipPool(path); - // but we also want to 404 if it exists but isn't in the silo - var link = db_1.db.ipPoolSilos.find(function (link) { return link.ip_pool_id === pool.id && link.silo_id === silo_1.defaultSilo.id; }); - if (!link) - throw (0, db_1.notFoundErr)(); - return __assign(__assign({}, pool), { is_default: link.is_default }); - }, - // TODO: require admin permissions for system IP pool endpoints - ipPoolView: function (_a) { - var path = _a.path; - return db_1.lookup.ipPool(path); - }, - ipPoolSiloList: function (_a) { - // TODO: paginated wants an id field, but this is a join table, so it has a - // composite pk - // return paginated(query, db.ipPoolResources) - var path = _a.path /*query*/; - var pool = db_1.lookup.ipPool(path); - var assocs = db_1.db.ipPoolSilos.filter(function (ipr) { return ipr.ip_pool_id === pool.id; }); - return { items: assocs }; - }, - ipPoolSiloLink: function (_a) { - var path = _a.path, body = _a.body; - var pool = db_1.lookup.ipPool(path); - var silo_id = db_1.lookup.silo({ silo: body.silo }).id; - var assoc = { - ip_pool_id: pool.id, - silo_id: silo_id, - is_default: body.is_default, - }; - var alreadyThere = db_1.db.ipPoolSilos.find(function (ips) { return ips.ip_pool_id === pool.id && ips.silo_id === silo_id; }); - // TODO: this matches current API logic but makes no sense because is_default - // could be different. Need to fix that. Should 400 or 409 on conflict. - if (!alreadyThere) - db_1.db.ipPoolSilos.push(assoc); - return assoc; - }, - ipPoolSiloUnlink: function (_a) { - var path = _a.path; - var pool = db_1.lookup.ipPool(path); - var silo = db_1.lookup.silo(path); - // ignore is_default when deleting, it's not part of the pk - db_1.db.ipPoolSilos = db_1.db.ipPoolSilos.filter(function (ips) { return !(ips.ip_pool_id === pool.id && ips.silo_id === silo.id); }); - return 204; - }, - ipPoolSiloUpdate: function (_a) { - var path = _a.path, body = _a.body; - var ipPoolSilo = db_1.lookup.ipPoolSiloLink(path); - // if we're setting default, we need to set is_default false on the existing default - if (body.is_default) { - var silo_2 = db_1.lookup.silo(path); - var existingDefault = db_1.db.ipPoolSilos.find(function (ips) { return ips.silo_id === silo_2.id && ips.is_default; }); - if (existingDefault) { - existingDefault.is_default = false; - } - } - ipPoolSilo.is_default = body.is_default; - return ipPoolSilo; - }, - ipPoolRangeList: function (_a) { - var path = _a.path, query = _a.query; - var pool = db_1.lookup.ipPool(path); - var ranges = db_1.db.ipPoolRanges.filter(function (r) { return r.ip_pool_id === pool.id; }); - return (0, util_2.paginated)(query, ranges); - }, - ipPoolRangeAdd: function (_a) { - var path = _a.path, body = _a.body; - var pool = db_1.lookup.ipPool(path); - var newRange = { - id: (0, uuid_1.v4)(), - ip_pool_id: pool.id, - range: body, - time_created: new Date().toISOString(), - }; - // TODO: validate that it doesn't overlap with existing ranges - db_1.db.ipPoolRanges.push(newRange); - return (0, msw_handlers_1.json)(newRange, { status: 201 }); - }, - ipPoolRangeRemove: function (_a) { - var path = _a.path, body = _a.body; - var pool = db_1.lookup.ipPool(path); - var idsToDelete = db_1.db.ipPoolRanges - .filter(function (r) { - return r.ip_pool_id === pool.id && - r.range.first === body.first && - r.range.last === body.last; - }) - .map(function (r) { return r.id; }); - // if nothing in the DB matches, 404 - if (idsToDelete.length === 0) - throw (0, db_1.notFoundErr)(); - db_1.db.ipPoolRanges = db_1.db.ipPoolRanges.filter(function (r) { return !idsToDelete.includes(r.id); }); - return 204; - }, - ipPoolCreate: function (_a) { - var body = _a.body; - (0, util_2.errIfExists)(db_1.db.ipPools, { name: body.name }, 'IP pool'); - var newPool = __assign(__assign({ id: (0, uuid_1.v4)() }, body), (0, util_2.getTimestamps)()); - db_1.db.ipPools.push(newPool); - return (0, msw_handlers_1.json)(newPool, { status: 201 }); - }, - ipPoolDelete: function (_a) { - var path = _a.path; - var pool = db_1.lookup.ipPool(path); - if (db_1.db.ipPoolRanges.some(function (r) { return r.ip_pool_id === pool.id; })) { - throw 'IP pool cannot be deleted while it contains IP ranges'; - } - // delete pools and silo links - db_1.db.ipPools = db_1.db.ipPools.filter(function (p) { return p.id !== pool.id; }); - db_1.db.ipPoolSilos = db_1.db.ipPoolSilos.filter(function (s) { return s.ip_pool_id !== pool.id; }); - return 204; - }, - ipPoolUpdate: function (_a) { - var path = _a.path, body = _a.body; - var pool = db_1.lookup.ipPool(path); - if (body.name) { - // only check for existing name if it's being changed - if (body.name !== pool.name) { - (0, util_2.errIfExists)(db_1.db.ipPools, { name: body.name }); - } - pool.name = body.name; - } - pool.description = body.description || ''; - return pool; - }, - projectPolicyView: function (_a) { - var path = _a.path; - var project = db_1.lookup.project(path); - var role_assignments = db_1.db.roleAssignments - .filter(function (r) { return r.resource_type === 'project' && r.resource_id === project.id; }) - .map(function (r) { return (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'); }); - return { role_assignments: role_assignments }; - }, - projectPolicyUpdate: function (_a) { - var body = _a.body, path = _a.path; - var project = db_1.lookup.project(path); - var newAssignments = body.role_assignments.map(function (r) { return (__assign({ resource_type: 'project', resource_id: project.id }, (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'))); }); - var unrelatedAssignments = db_1.db.roleAssignments.filter(function (r) { return !(r.resource_type === 'project' && r.resource_id === project.id); }); - db_1.db.roleAssignments = __spreadArray(__spreadArray([], unrelatedAssignments, true), newAssignments, true); - // TODO: Is this the right thing to return? - return body; - }, - snapshotList: function (params) { - var project = db_1.lookup.project(params.query); - var snapshots = db_1.db.snapshots.filter(function (i) { return i.project_id === project.id; }); - return (0, util_2.paginated)(params.query, snapshots); - }, - snapshotCreate: function (_a) { - var body = _a.body, query = _a.query; - var project = db_1.lookup.project(query); - if (body.disk === 'disk-snapshot-error') { - throw 'Cannot snapshot disk'; - } - (0, util_2.errIfExists)(db_1.db.snapshots, { name: body.name }); - var disk = db_1.lookup.disk(__assign(__assign({}, query), { disk: body.disk })); - if (!api_1.diskCan.snapshot(disk)) { - throw 'Cannot snapshot disk in state ' + disk.state.state; - } - var newSnapshot = __assign(__assign(__assign({ id: (0, uuid_1.v4)() }, body), (0, util_2.getTimestamps)()), { state: 'ready', project_id: project.id, disk_id: disk.id, size: disk.size }); - db_1.db.snapshots.push(newSnapshot); - return (0, msw_handlers_1.json)(newSnapshot, { status: 201 }); - }, - snapshotView: function (_a) { - var path = _a.path, query = _a.query; - return db_1.lookup.snapshot(__assign(__assign({}, path), query)); - }, - snapshotDelete: function (_a) { - var path = _a.path, query = _a.query; - if (path.snapshot === 'delete-500') - return 500; - var snapshot = db_1.lookup.snapshot(__assign(__assign({}, path), query)); - db_1.db.snapshots = db_1.db.snapshots.filter(function (s) { return s.id !== snapshot.id; }); - return 204; - }, - utilizationView: function () { - var _a = (0, db_1.utilizationForSilo)(silo_1.defaultSilo), capacity = _a.allocated, provisioned = _a.provisioned; - return { capacity: capacity, provisioned: provisioned }; - }, - siloUtilizationView: function (_a) { - var path = _a.path; - var silo = db_1.lookup.silo(path); - return (0, db_1.utilizationForSilo)(silo); - }, - siloUtilizationList: function (_a) { - var query = _a.query; - var _b = (0, util_2.paginated)(query, db_1.db.silos), silos = _b.items, nextPage = _b.nextPage; - return { - items: silos.map(db_1.utilizationForSilo), - nextPage: nextPage, - }; - }, - vpcList: function (_a) { - var query = _a.query; - var project = db_1.lookup.project(query); - var vpcs = db_1.db.vpcs.filter(function (v) { return v.project_id === project.id; }); - return (0, util_2.paginated)(query, vpcs); - }, - vpcCreate: function (_a) { - var body = _a.body, query = _a.query; - var project = db_1.lookup.project(query); - (0, util_2.errIfExists)(db_1.db.vpcs, { name: body.name }); - var newVpc = __assign(__assign(__assign({ id: (0, uuid_1.v4)(), project_id: project.id, system_router_id: (0, uuid_1.v4)() }, body), { - // API is supposed to generate one if none provided. close enough - ipv6_prefix: body.ipv6_prefix || 'fd2d:4569:88b2::/64' }), (0, util_2.getTimestamps)()); - db_1.db.vpcs.push(newVpc); - // Also create a default subnet - var newSubnet = __assign({ id: (0, uuid_1.v4)(), name: 'default', vpc_id: newVpc.id, ipv6_block: 'fd2d:4569:88b1::/64', description: '', ipv4_block: '' }, (0, util_2.getTimestamps)()); - db_1.db.vpcSubnets.push(newSubnet); - return (0, msw_handlers_1.json)(newVpc, { status: 201 }); - }, - vpcView: function (_a) { - var path = _a.path, query = _a.query; - return db_1.lookup.vpc(__assign(__assign({}, path), query)); - }, - vpcUpdate: function (_a) { - var body = _a.body, path = _a.path, query = _a.query; - var vpc = db_1.lookup.vpc(__assign(__assign({}, path), query)); - if (body.name) { - vpc.name = body.name; - } - if (typeof body.description === 'string') { - vpc.description = body.description; - } - if (body.dns_name) { - vpc.dns_name = body.dns_name; - } - return vpc; - }, - vpcDelete: function (_a) { - var path = _a.path, query = _a.query; - var vpc = db_1.lookup.vpc(__assign(__assign({}, path), query)); - db_1.db.vpcs = db_1.db.vpcs.filter(function (v) { return v.id !== vpc.id; }); - db_1.db.vpcSubnets = db_1.db.vpcSubnets.filter(function (s) { return s.vpc_id !== vpc.id; }); - db_1.db.vpcFirewallRules = db_1.db.vpcFirewallRules.filter(function (r) { return r.vpc_id !== vpc.id; }); - return 204; - }, - vpcFirewallRulesView: function (_a) { - var query = _a.query; - var vpc = db_1.lookup.vpc(query); - var rules = db_1.db.vpcFirewallRules.filter(function (r) { return r.vpc_id === vpc.id; }); - return { rules: (0, util_1.sortBy)(rules, function (r) { return r.name; }) }; - }, - vpcFirewallRulesUpdate: function (_a) { - var body = _a.body, query = _a.query; - var vpc = db_1.lookup.vpc(query); - var rules = body.rules.map(function (rule) { return (__assign(__assign({ vpc_id: vpc.id, id: (0, uuid_1.v4)() }, rule), (0, util_2.getTimestamps)())); }); - // replace existing rules for this VPC with the new ones - db_1.db.vpcFirewallRules = __spreadArray(__spreadArray([], db_1.db.vpcFirewallRules.filter(function (r) { return r.vpc_id !== vpc.id; }), true), rules, true); - return { rules: (0, util_1.sortBy)(rules, function (r) { return r.name; }) }; - }, - vpcSubnetList: function (_a) { - var query = _a.query; - var vpc = db_1.lookup.vpc(query); - var subnets = db_1.db.vpcSubnets.filter(function (s) { return s.vpc_id === vpc.id; }); - return (0, util_2.paginated)(query, subnets); - }, - vpcSubnetCreate: function (_a) { - var body = _a.body, query = _a.query; - var vpc = db_1.lookup.vpc(query); - (0, util_2.errIfExists)(db_1.db.vpcSubnets, { vpc_id: vpc.id, name: body.name }); - // TODO: Create a route for the subnet in the default router - var newSubnet = __assign(__assign(__assign({ id: (0, uuid_1.v4)(), vpc_id: vpc.id }, body), { - // required in subnet create but not in update, so we need a fallback. - // API says "A random `/64` block will be assigned if one is not - // provided." Our fallback is not random, but it should be good enough. - ipv6_block: body.ipv6_block || 'fd2d:4569:88b1::/64' }), (0, util_2.getTimestamps)()); - db_1.db.vpcSubnets.push(newSubnet); - return (0, msw_handlers_1.json)(newSubnet, { status: 201 }); - }, - vpcSubnetView: function (_a) { - var path = _a.path, query = _a.query; - return db_1.lookup.vpcSubnet(__assign(__assign({}, path), query)); - }, - vpcSubnetUpdate: function (_a) { - var body = _a.body, path = _a.path, query = _a.query; - var subnet = db_1.lookup.vpcSubnet(__assign(__assign({}, path), query)); - if (body.name) { - subnet.name = body.name; - } - if (typeof body.description === 'string') { - subnet.description = body.description; - } - return subnet; - }, - vpcSubnetDelete: function (_a) { - var path = _a.path, query = _a.query; - var subnet = db_1.lookup.vpcSubnet(__assign(__assign({}, path), query)); - db_1.db.vpcSubnets = db_1.db.vpcSubnets.filter(function (s) { return s.id !== subnet.id; }); - return 204; - }, - vpcSubnetListNetworkInterfaces: function (_a) { - var path = _a.path, query = _a.query; - var subnet = db_1.lookup.vpcSubnet(__assign(__assign({}, path), query)); - var nics = db_1.db.networkInterfaces.filter(function (n) { return n.subnet_id === subnet.id; }); - return (0, util_2.paginated)(query, nics); - }, - sledPhysicalDiskList: function (_a) { - var path = _a.path, query = _a.query, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - var sled = db_1.lookup.sled(path); - var disks = db_1.db.physicalDisks.filter(function (n) { return n.sled_id === sled.id; }); - return (0, util_2.paginated)(query, disks); - }, - physicalDiskList: function (_a) { - var query = _a.query, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - return (0, util_2.paginated)(query, db_1.db.physicalDisks); - }, - policyView: function () { - // assume we're in the default silo - var siloId = silo_1.defaultSilo.id; - var role_assignments = db_1.db.roleAssignments - .filter(function (r) { return r.resource_type === 'silo' && r.resource_id === siloId; }) - .map(function (r) { return (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'); }); - return { role_assignments: role_assignments }; - }, - policyUpdate: function (_a) { - var body = _a.body; - var siloId = silo_1.defaultSilo.id; - var newAssignments = body.role_assignments.map(function (r) { return (__assign({ resource_type: 'silo', resource_id: siloId }, (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'))); }); - var unrelatedAssignments = db_1.db.roleAssignments.filter(function (r) { return !(r.resource_type === 'silo' && r.resource_id === siloId); }); - db_1.db.roleAssignments = __spreadArray(__spreadArray([], unrelatedAssignments, true), newAssignments, true); - return body; - }, - rackList: function (_a) { - var query = _a.query, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - return (0, util_2.paginated)(query, db_1.db.racks); - }, - currentUserView: function (_a) { - var cookies = _a.cookies; - return __assign(__assign({}, (0, util_2.currentUser)(cookies)), { silo_name: silo_1.defaultSilo.name }); - }, - currentUserGroups: function (_a) { - var cookies = _a.cookies; - var user = (0, util_2.currentUser)(cookies); - var memberships = db_1.db.groupMemberships.filter(function (gm) { return gm.userId === user.id; }); - var groupIds = new Set(memberships.map(function (gm) { return gm.groupId; })); - var groups = db_1.db.userGroups.filter(function (g) { return groupIds.has(g.id); }); - return { items: groups }; - }, - currentUserSshKeyList: function (_a) { - var query = _a.query, cookies = _a.cookies; - var user = (0, util_2.currentUser)(cookies); - var keys = db_1.db.sshKeys.filter(function (k) { return k.silo_user_id === user.id; }); - return (0, util_2.paginated)(query, keys); - }, - currentUserSshKeyCreate: function (_a) { - var body = _a.body, cookies = _a.cookies; - var user = (0, util_2.currentUser)(cookies); - (0, util_2.errIfExists)(db_1.db.sshKeys, { silo_user_id: user.id, name: body.name }); - var newSshKey = __assign(__assign({ id: (0, uuid_1.v4)(), silo_user_id: user.id }, body), (0, util_2.getTimestamps)()); - db_1.db.sshKeys.push(newSshKey); - return (0, msw_handlers_1.json)(newSshKey, { status: 201 }); - }, - currentUserSshKeyView: function (_a) { - var path = _a.path; - return db_1.lookup.sshKey(path); - }, - currentUserSshKeyDelete: function (_a) { - var path = _a.path; - var sshKey = db_1.lookup.sshKey(path); - db_1.db.sshKeys = db_1.db.sshKeys.filter(function (i) { return i.id !== sshKey.id; }); - return 204; - }, - sledView: function (_a) { - var path = _a.path, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - return db_1.lookup.sled(path); - }, - sledList: function (_a) { - var query = _a.query, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - return (0, util_2.paginated)(query, db_1.db.sleds); - }, - sledInstanceList: function (_a) { - var query = _a.query, path = _a.path, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - var sled = (0, db_1.lookupById)(db_1.db.sleds, path.sledId); - return (0, util_2.paginated)(query, db_1.db.instances.map(function (i) { - var project = (0, db_1.lookupById)(db_1.db.projects, i.project_id); - return __assign(__assign({}, (0, util_1.pick)(i, 'id', 'name', 'time_created', 'time_modified', 'memory', 'ncpus')), { state: 'running', active_sled_id: sled.id, project_name: project.name, silo_name: silo_1.defaultSilo.name }); - })); - }, - siloList: function (_a) { - var query = _a.query, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - return (0, util_2.paginated)(query, db_1.db.silos); - }, - siloCreate: function (_a) { - var body = _a.body, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - (0, util_2.errIfExists)(db_1.db.silos, { name: body.name }); - var newSilo = __assign(__assign(__assign({ id: (0, uuid_1.v4)() }, (0, util_2.getTimestamps)()), body), { mapped_fleet_roles: body.mapped_fleet_roles || {} }); - db_1.db.silos.push(newSilo); - return (0, msw_handlers_1.json)(newSilo, { status: 201 }); - }, - siloView: function (_a) { - var path = _a.path, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - return db_1.lookup.silo(path); - }, - siloDelete: function (_a) { - var path = _a.path, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - var silo = db_1.lookup.silo(path); - db_1.db.silos = db_1.db.silos.filter(function (i) { return i.id !== silo.id; }); - db_1.db.ipPoolSilos = db_1.db.ipPoolSilos.filter(function (i) { return i.silo_id !== silo.id; }); - return 204; - }, - siloIdentityProviderList: function (_a) { - var query = _a.query, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - var silo = db_1.lookup.silo(query); - var idps = db_1.db.identityProviders.filter(function (_a) { - var siloId = _a.siloId; - return siloId === silo.id; - }).map(silo_1.toIdp); - return { items: idps }; - }, - samlIdentityProviderCreate: function (_a) { - var _b; - var query = _a.query, body = _a.body, cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - var silo = db_1.lookup.silo(query); - // this is a bit silly, but errIfExists doesn't handle nested keys like - // provider.name, so to do the check we make a flatter object - (0, util_2.errIfExists)(db_1.db.identityProviders.map(function (_a) { - var siloId = _a.siloId, provider = _a.provider; - return ({ siloId: siloId, name: provider.name }); - }), { siloId: silo.id, name: body.name }); - // we just decode to string and store that, which is probably fine for local - // dev, but note that the API decodes to bytes and passes that to - // https://docs.rs/openssl/latest/openssl/x509/struct.X509.html#method.from_der - // and that will error if can't be parsed that way - var public_cert = (_b = body.signing_keypair) === null || _b === void 0 ? void 0 : _b.public_cert; - public_cert = public_cert ? atob(public_cert) : undefined; - // we ignore the private key because it's not returned in the get response, - // so you'll never see it again. But worth noting that in the real thing - // it is parsed with this - // https://docs.rs/openssl/latest/openssl/rsa/struct.Rsa.html#method.private_key_from_der - var provider = __assign(__assign(__assign({ id: (0, uuid_1.v4)() }, (0, util_1.pick)(body, 'name', 'acs_url', 'description', 'idp_entity_id', 'slo_url', 'sp_client_id', 'technical_contact_email')), { public_cert: public_cert }), (0, util_2.getTimestamps)()); - db_1.db.identityProviders.push({ type: 'saml', siloId: silo.id, provider: provider }); - return provider; - }, - samlIdentityProviderView: function (_a) { - var path = _a.path, query = _a.query; - return db_1.lookup.samlIdp(__assign(__assign({}, path), query)); - }, - userList: function (_a) { - var query = _a.query; - // query.group is validated by generated code to be a UUID if present - if (query.group) { - var group_1 = (0, db_1.lookupById)(db_1.db.userGroups, query.group); // 404 if doesn't exist - var memberships = db_1.db.groupMemberships.filter(function (gm) { return gm.groupId === group_1.id; }); - var userIds_1 = new Set(memberships.map(function (gm) { return gm.userId; })); - var users = db_1.db.users.filter(function (u) { return userIds_1.has(u.id); }); - return (0, util_2.paginated)(query, users); - } - return (0, util_2.paginated)(query, db_1.db.users); - }, - systemPolicyView: function (_a) { - var cookies = _a.cookies; - (0, util_2.requireFleetViewer)(cookies); - var role_assignments = db_1.db.roleAssignments - .filter(function (r) { return r.resource_type === 'fleet' && r.resource_id === api_1.FLEET_ID; }) - .map(function (r) { return (0, util_1.pick)(r, 'identity_id', 'identity_type', 'role_name'); }); - return { role_assignments: role_assignments }; - }, - systemMetric: function (params) { - (0, util_2.requireFleetViewer)(params.cookies); - return (0, util_2.handleMetrics)(params); - }, - siloMetric: util_2.handleMetrics, - // Misc endpoints we're not using yet in the console - certificateCreate: util_2.NotImplemented, - certificateDelete: util_2.NotImplemented, - certificateList: util_2.NotImplemented, - certificateView: util_2.NotImplemented, - floatingIpAttach: util_2.NotImplemented, - floatingIpCreate: util_2.NotImplemented, - floatingIpDelete: util_2.NotImplemented, - floatingIpDetach: util_2.NotImplemented, - floatingIpList: util_2.NotImplemented, - floatingIpView: util_2.NotImplemented, - instanceEphemeralIpDetach: util_2.NotImplemented, - instanceEphemeralIpAttach: util_2.NotImplemented, - instanceMigrate: util_2.NotImplemented, - instanceSerialConsoleStream: util_2.NotImplemented, - instanceSshPublicKeyList: util_2.NotImplemented, - ipPoolServiceRangeAdd: util_2.NotImplemented, - ipPoolServiceRangeList: util_2.NotImplemented, - ipPoolServiceRangeRemove: util_2.NotImplemented, - ipPoolServiceView: util_2.NotImplemented, - localIdpUserCreate: util_2.NotImplemented, - localIdpUserDelete: util_2.NotImplemented, - localIdpUserSetPassword: util_2.NotImplemented, - loginSaml: util_2.NotImplemented, - logout: util_2.NotImplemented, - networkingAddressLotBlockList: util_2.NotImplemented, - networkingAddressLotCreate: util_2.NotImplemented, - networkingAddressLotDelete: util_2.NotImplemented, - networkingAddressLotList: util_2.NotImplemented, - networkingBfdDisable: util_2.NotImplemented, - networkingBfdEnable: util_2.NotImplemented, - networkingBfdStatus: util_2.NotImplemented, - networkingBgpAnnounceSetCreate: util_2.NotImplemented, - networkingBgpAnnounceSetDelete: util_2.NotImplemented, - networkingBgpAnnounceSetList: util_2.NotImplemented, - networkingBgpConfigCreate: util_2.NotImplemented, - networkingBgpConfigDelete: util_2.NotImplemented, - networkingBgpConfigList: util_2.NotImplemented, - networkingBgpImportedRoutesIpv4: util_2.NotImplemented, - networkingBgpStatus: util_2.NotImplemented, - networkingLoopbackAddressCreate: util_2.NotImplemented, - networkingLoopbackAddressDelete: util_2.NotImplemented, - networkingLoopbackAddressList: util_2.NotImplemented, - networkingSwitchPortApplySettings: util_2.NotImplemented, - networkingSwitchPortClearSettings: util_2.NotImplemented, - networkingSwitchPortList: util_2.NotImplemented, - networkingSwitchPortSettingsCreate: util_2.NotImplemented, - networkingSwitchPortSettingsDelete: util_2.NotImplemented, - networkingSwitchPortSettingsView: util_2.NotImplemented, - networkingSwitchPortSettingsList: util_2.NotImplemented, - rackView: util_2.NotImplemented, - roleList: util_2.NotImplemented, - roleView: util_2.NotImplemented, - siloPolicyUpdate: util_2.NotImplemented, - siloPolicyView: util_2.NotImplemented, - siloQuotasUpdate: util_2.NotImplemented, - siloQuotasView: util_2.NotImplemented, - siloUserList: util_2.NotImplemented, - siloUserView: util_2.NotImplemented, - sledAdd: util_2.NotImplemented, - sledListUninitialized: util_2.NotImplemented, - sledSetProvisionState: util_2.NotImplemented, - switchList: util_2.NotImplemented, - switchView: util_2.NotImplemented, - systemPolicyUpdate: util_2.NotImplemented, - systemQuotasList: util_2.NotImplemented, - userBuiltinList: util_2.NotImplemented, - userBuiltinView: util_2.NotImplemented, -}); diff --git a/libs/api-mocks/msw/util.js b/libs/api-mocks/msw/util.js deleted file mode 100644 index 6c061c0049..0000000000 --- a/libs/api-mocks/msw/util.js +++ /dev/null @@ -1,314 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.requireRole = exports.requireFleetViewer = exports.userHasRole = exports.currentUser = exports.MSW_USER_COOKIE = exports.handleMetrics = exports.generateUtilization = exports.errIfInvalidDiskSize = exports.errIfExists = exports.internalError = exports.NotImplemented = exports.unavailableErr = exports.getTimestamps = exports.getStartAndEndTime = exports.repeat = exports.paginated = exports.json = void 0; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -var date_fns_1 = require("date-fns"); -var api_1 = require("@oxide/api"); -var msw_handlers_1 = require("@oxide/gen/msw-handlers"); -var util_1 = require("@oxide/util"); -var metrics_1 = require("../metrics"); -var db_1 = require("./db"); -var msw_handlers_2 = require("@oxide/gen/msw-handlers"); -Object.defineProperty(exports, "json", { enumerable: true, get: function () { return msw_handlers_2.json; } }); -var paginated = function (params, items) { - var _a = params || {}, _b = _a.limit, limit = _b === void 0 ? 100 : _b, pageToken = _a.pageToken; - var startIndex = pageToken ? items.findIndex(function (i) { return i.id === pageToken; }) : 0; - startIndex = startIndex < 0 ? 0 : startIndex; - if (startIndex > items.length) { - return { - items: [], - nextPage: null, - }; - } - if (limit + startIndex >= items.length) { - return { - items: items.slice(startIndex), - nextPage: null, - }; - } - return { - items: items.slice(startIndex, startIndex + limit), - nextPage: "".concat(items[startIndex + limit].id), - }; -}; -exports.paginated = paginated; -// make a bunch of copies of an object with different names and IDs. useful for -// testing pagination -var repeat = function (obj, n) { - return new Array(n).fill(0).map(function (_, i) { return (__assign(__assign({}, obj), { id: obj.id + i, name: obj.name + i })); }); -}; -exports.repeat = repeat; -function getStartAndEndTime(params) { - // if no start time or end time, give the last 24 hours. in this case the - // API will give all data available for the metric (paginated of course), - // so essentially we're pretending the last 24 hours just happens to be - // all the data. if we have an end time but no start time, same deal, pretend - // 24 hours before the given end time is where it starts - var now = new Date(); - var _a = params.endTime, endTime = _a === void 0 ? now : _a, _b = params.startTime, startTime = _b === void 0 ? (0, date_fns_1.subHours)(endTime, 24) : _b; - return { startTime: startTime, endTime: endTime }; -} -exports.getStartAndEndTime = getStartAndEndTime; -function getTimestamps() { - var now = new Date().toISOString(); - return { time_created: now, time_modified: now }; -} -exports.getTimestamps = getTimestamps; -var unavailableErr = function () { - return (0, msw_handlers_1.json)({ error_code: 'ServiceUnavailable' }, { status: 503 }); -}; -exports.unavailableErr = unavailableErr; -var NotImplemented = function () { - // This doesn't just return the response because it broadens the type to be usable - // directly as a handler - throw (0, msw_handlers_1.json)({ error_code: 'NotImplemented' }, { status: 501 }); -}; -exports.NotImplemented = NotImplemented; -var internalError = function () { return (0, msw_handlers_1.json)({ error_code: 'InternalError' }, { status: 500 }); }; -exports.internalError = internalError; -var errIfExists = function (collection, match, resourceLabel) { - if (resourceLabel === void 0) { resourceLabel = 'resource'; } - if (collection.some(function (item) { - return Object.entries(match).every(function (_a) { - var key = _a[0], value = _a[1]; - return item[key] === value; - }); - })) { - var name_1 = 'name' in match ? match.name : 'id' in match ? match.id : ''; - throw (0, msw_handlers_1.json)({ - error_code: 'ObjectAlreadyExists', - message: "already exists: ".concat(resourceLabel, " \"").concat(name_1, "\""), - }, { status: 400 }); - } -}; -exports.errIfExists = errIfExists; -var errIfInvalidDiskSize = function (disk) { - var _a, _b, _c, _d; - var source = disk.disk_source; - if (disk.size < api_1.MIN_DISK_SIZE_GiB * util_1.GiB) { - throw "Disk size must be greater than or equal to ".concat(api_1.MIN_DISK_SIZE_GiB, " GiB"); - } - if (disk.size > api_1.MAX_DISK_SIZE_GiB * util_1.GiB) { - throw "Disk size must be less than or equal to ".concat(api_1.MAX_DISK_SIZE_GiB, " GiB"); - } - if (source.type === 'snapshot') { - var snapshotSize = (_b = (_a = db_1.db.snapshots.find(function (s) { return source.snapshot_id === s.id; })) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0; - if (disk.size >= snapshotSize) - return; - throw 'Disk size must be greater than or equal to the snapshot size'; - } - if (source.type === 'image') { - var imageSize = (_d = (_c = db_1.db.images.find(function (i) { return source.image_id === i.id; })) === null || _c === void 0 ? void 0 : _c.size) !== null && _d !== void 0 ? _d : 0; - if (disk.size >= imageSize) - return; - throw 'Disk size must be greater than or equal to the image size'; - } -}; -exports.errIfInvalidDiskSize = errIfInvalidDiskSize; -var Rando = /** @class */ (function () { - function Rando(seed, a, c, m) { - if (a === void 0) { a = 1664525; } - if (c === void 0) { c = 1013904223; } - if (m === void 0) { m = Math.pow(2, 32); } - this.seed = seed; - this.a = a; - this.c = c; - this.m = m; - } - Rando.prototype.next = function () { - this.seed = (this.a * this.seed + this.c) % this.m; - return this.seed / this.m; - }; - return Rando; -}()); -function generateUtilization(metricName, startTime, endTime, sleds) { - // generate data from at most 90 days ago no matter how early start time is - var adjStartTime = new Date(Math.max(startTime.getTime(), Date.now() - 1000 * 60 * 60 * 24 * 90)); - var capacity = (0, api_1.totalCapacity)(sleds.map(function (s) { return ({ - usableHardwareThreads: s.usable_hardware_threads, - usablePhysicalRam: s.usable_physical_ram, - }); })); - var cap = metricName === 'cpus_provisioned' - ? capacity.cpu - : metricName === 'virtual_disk_space_provisioned' - ? capacity.disk_tib * util_1.TiB - : capacity.ram_gib * util_1.GiB; - var metricNameSeed = Array.from(metricName).reduce(function (acc, char) { return acc + char.charCodeAt(0); }, 0); - var rando = new Rando(adjStartTime.getTime() + metricNameSeed); - var diff = Math.abs((0, date_fns_1.differenceInSeconds)(adjStartTime, endTime)); - // How many quarter hour chunks in the date range - // Use that as how often to offset the data to seem - // more realistic - var timeInterval = diff / 900; - // If the data is the following length - var dataCount = 1000; - // How far along the array should we do something - var valueInterval = Math.floor(dataCount / timeInterval); - // Pick a reasonable start value - var startVal = 500; - var values = new Array(dataCount); - values[0] = startVal; - var x = 0; - for (var i = 1; i < values.length; i++) { - values[i] = values[i - 1]; - if (x === valueInterval) { - // Do something 3/4 of the time - var offset = 0; - var random = rando.next(); - var threshold = i < 250 || (i > 500 && i < 750) ? 1 : 0.375; - if (random < threshold) { - var amount = 50; - offset = Math.floor(random * amount); - if (random < threshold / 2.5) { - offset = offset * -1; - } - } - if (random > 0.72) { - values[i] += offset; - } - else { - values[i] = Math.max(values[i] - offset, 0); - } - x = 0; - } - else { - x++; - } - } - // Find the current maximum value in the generated data - var currentMax = Math.max.apply(Math, values); - // Normalize the data to sit within the range of 0 to overall capacity - var randomFactor = Math.random() * (1 - 0.33) + 0.33; - var normalizedValues = values.map(function (value) { - var v = (value / currentMax) * cap * randomFactor; - if (metricName === 'cpus_provisioned') { - // CPU utilization should be whole numbers - v = Math.floor(v); - } - return v; - }); - return normalizedValues; -} -exports.generateUtilization = generateUtilization; -function handleMetrics(_a) { - var metricName = _a.path.metricName, query = _a.query; - var _b = getStartAndEndTime(query), startTime = _b.startTime, endTime = _b.endTime; - if (endTime <= startTime) - return { items: [] }; - var dataPoints = generateUtilization(metricName, startTime, endTime, db_1.db.sleds); - // Important to remember (but probably not important enough to change) that - // this works quite differently from the real API, which is going to be - // querying clickhouse with some fixed set of data, and when it starts from - // the end (order == 'descending') it's going to get data points starting - // from the end. When it starts from the beginning it gets data points from - // the beginning. For our fake data, we just generate the same set of data - // points spanning the whole time range, then reverse the list if necessary - // and take the first N=limit data points. - var items = (0, metrics_1.genI64Data)(dataPoints, startTime, endTime); - if (query.order === 'descending') { - items.reverse(); - } - if (typeof query.limit === 'number') { - items = items.slice(0, query.limit); - } - return { items: items }; -} -exports.handleMetrics = handleMetrics; -exports.MSW_USER_COOKIE = 'msw-user'; -/** - * Look up user by display name in cookie. Return the first user if cookie empty - * or name not found. We're using display name to make it easier to set the - * cookie by hand, because there is no way yet to pick a user through the UI. - * - * If cookie is empty or name is not found, return the first user in the list, - * who has admin on everything. - */ -function currentUser(cookies) { - var _a; - var name = cookies[exports.MSW_USER_COOKIE]; - return (_a = db_1.db.users.find(function (u) { return u.display_name === name; })) !== null && _a !== void 0 ? _a : db_1.db.users[0]; -} -exports.currentUser = currentUser; -/** - * Given a role A, get a list of the roles (including A) that confer *at least* - * the powers of A. - */ -// could implement with `takeUntil(allRoles, r => r === role)`, but that is so -// much harder to understand -var roleOrStronger = { - viewer: ['viewer', 'collaborator', 'admin'], - collaborator: ['collaborator', 'admin'], - admin: ['admin'], -}; -/** - * Determine whether a user has a role at least as strong as `role` on the - * specified resource. Note that this does not yet do parent-child inheritance - * like Nexus does, i.e., if a user has collaborator on a silo, then it inherits - * collaborator on all projects in the silo even if it has no explicit role on - * those projects. This does NOT do that. - */ -function userHasRole(user, resourceType, resourceId, role) { - var userGroupIds = db_1.db.groupMemberships - .filter(function (gm) { return gm.userId === user.id; }) - .map(function (gm) { return db_1.db.userGroups.find(function (g) { return g.id === gm.groupId; }); }) - .filter(util_1.isTruthy) - .map(function (g) { return g.id; }); - /** All actors with *at least* the specified role on the resource */ - var actorsWithRole = db_1.db.roleAssignments - .filter(function (ra) { - return ra.resource_type === resourceType && - ra.resource_id === resourceId && - roleOrStronger[role].includes(ra.role_name); - }) - .map(function (ra) { return ra.identity_id; }); - // user has role if their own ID or any of their groups is associated with the role - return __spreadArray([user.id], userGroupIds, true).some(function (id) { return actorsWithRole.includes(id); }); -} -exports.userHasRole = userHasRole; -/** - * Determine whether current user has fleet viewer permissions by looking for - * fleet roles for the user as well as for the user's groups. Do nothing if yes, - * throw 403 if no. - */ -function requireFleetViewer(cookies) { - requireRole(cookies, 'fleet', api_1.FLEET_ID, 'viewer'); -} -exports.requireFleetViewer = requireFleetViewer; -/** - * Determine whether current user has a role on a resource by looking roles - * for the user as well as for the user's groups. Do nothing if yes, throw 403 - * if no. - */ -function requireRole(cookies, resourceType, resourceId, role) { - var user = currentUser(cookies); - // should it 404? I think the API is a mix - if (!userHasRole(user, resourceType, resourceId, role)) - throw 403; -} -exports.requireRole = requireRole; diff --git a/libs/api-mocks/network-interface.js b/libs/api-mocks/network-interface.js deleted file mode 100644 index 99afe903e1..0000000000 --- a/libs/api-mocks/network-interface.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.networkInterface = void 0; -var instance_1 = require("./instance"); -var vpc_1 = require("./vpc"); -exports.networkInterface = { - id: 'f6d63297-287c-4035-b262-e8303cfd6a0f', - name: 'my-nic', - description: 'a network interface', - primary: true, - instance_id: instance_1.instance.id, - ip: '172.30.0.10', - mac: '', - subnet_id: vpc_1.vpcSubnet.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - vpc_id: vpc_1.vpc.id, -}; diff --git a/libs/api-mocks/physical-disk.js b/libs/api-mocks/physical-disk.js deleted file mode 100644 index 5877f736c9..0000000000 --- a/libs/api-mocks/physical-disk.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.physicalDisks = void 0; -exports.physicalDisks = [ - { - id: 'd2cf9763-cfce-4291-8531-614c8b4aa632', - form_factor: 'u2', - model: 'MTFDKBG800TDZ-1AZ1ZAB', - serial: '0C98MRMBK64', - vendor: '0634', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - sled_id: 'c2519937-44a4-493b-9b38-5c337c597d08', - }, - { - id: '9d43adfe-c46a-4a33-b060-146cbd48b767', - form_factor: 'u2', - model: 'MTFDKBG800TDZ-1AZ1ZAB', - serial: 'A9GCW7OS3HT', - vendor: '0634', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - sled_id: 'c2519937-44a4-493b-9b38-5c337c597d08', - }, - { - id: 'f253c29f-321b-46d0-a132-6235cc63e3d2', - form_factor: 'u2', - model: 'MTFDKBG800TDZ-1AZ1ZAB', - serial: '0V2L160OZ9J', - vendor: '0634', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - sled_id: 'c2519937-44a4-493b-9b38-5c337c597d08', - }, - { - id: '0591ae13-3c72-4701-a801-20e44f809496', - form_factor: 'm2', - model: 'MTFDKBG800TDZ-1AZ1ZAB', - serial: 'CA73ANUYLJ9', - vendor: '0634', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - sled_id: 'c2519937-44a4-493b-9b38-5c337c597d08', - }, -]; diff --git a/libs/api-mocks/project.js b/libs/api-mocks/project.js deleted file mode 100644 index 2d8dbcf501..0000000000 --- a/libs/api-mocks/project.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.projectRolePolicy = exports.projects = exports.project2 = exports.project = void 0; -var user_1 = require("./user"); -exports.project = { - id: '5fbab865-3d09-4c16-a22f-ca9c312b0286', - name: 'mock-project', - description: 'a fake project', - time_created: new Date(2021, 0, 1).toISOString(), - time_modified: new Date(2021, 0, 2).toISOString(), -}; -exports.project2 = { - id: 'e7bd835e-831e-4257-b600-f1db32844c8c', - name: 'other-project', - description: 'another fake project', - time_created: new Date(2021, 0, 15).toISOString(), - time_modified: new Date(2021, 0, 16).toISOString(), -}; -exports.projects = [exports.project, exports.project2]; -exports.projectRolePolicy = { - role_assignments: [ - { - identity_id: user_1.user1.id, - identity_type: 'silo_user', - role_name: 'admin', - }, - ], -}; diff --git a/libs/api-mocks/rack.js b/libs/api-mocks/rack.js deleted file mode 100644 index 3b51176588..0000000000 --- a/libs/api-mocks/rack.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.racks = exports.rack = void 0; -exports.rack = { - id: '6fbafcc7-1626-4785-be65-e212f8ad66d0', - time_created: new Date(2021, 0, 1).toISOString(), - time_modified: new Date(2021, 0, 2).toISOString(), -}; -exports.racks = [exports.rack]; diff --git a/libs/api-mocks/role-assignment.js b/libs/api-mocks/role-assignment.js deleted file mode 100644 index 81a09225a1..0000000000 --- a/libs/api-mocks/role-assignment.js +++ /dev/null @@ -1,52 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.roleAssignments = void 0; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -var api_1 = require("@oxide/api"); -var project_1 = require("./project"); -var silo_1 = require("./silo"); -var user_1 = require("./user"); -var user_group_1 = require("./user-group"); -exports.roleAssignments = [ - { - resource_type: 'fleet', - resource_id: api_1.FLEET_ID, - identity_id: user_1.user1.id, - identity_type: 'silo_user', - role_name: 'admin', - }, - { - resource_type: 'silo', - resource_id: silo_1.defaultSilo.id, - identity_id: user_group_1.userGroup3.id, - identity_type: 'silo_group', - role_name: 'collaborator', - }, - { - resource_type: 'silo', - resource_id: silo_1.defaultSilo.id, - identity_id: user_1.user1.id, - identity_type: 'silo_user', - role_name: 'admin', - }, - { - resource_type: 'project', - resource_id: project_1.project.id, - identity_id: user_1.user3.id, - identity_type: 'silo_user', - role_name: 'collaborator', - }, - { - resource_type: 'project', - resource_id: project_1.project.id, - identity_id: user_group_1.userGroup2.id, - identity_type: 'silo_group', - role_name: 'viewer', - }, -]; diff --git a/libs/api-mocks/serial.js b/libs/api-mocks/serial.js deleted file mode 100644 index 49afc6900a..0000000000 --- a/libs/api-mocks/serial.js +++ /dev/null @@ -1,3480 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.serial = void 0; -exports.serial = { - last_byte_offset: 7351, - data: [ - 27, - 91, - 50, - 74, - 27, - 91, - 48, - 49, - 59, - 48, - 49, - 72, - 27, - 91, - 61, - 51, - 104, - 27, - 91, - 50, - 74, - 27, - 91, - 48, - 49, - 59, - 48, - 49, - 72, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 53, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 55, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 53, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 55, - 109, - 27, - 91, - 52, - 48, - 109, - 66, - 100, - 115, - 68, - 120, - 101, - 58, - 32, - 108, - 111, - 97, - 100, - 105, - 110, - 103, - 32, - 66, - [111, 2], - 116, - [48, 3], - 49, - 32, - 34, - 85, - 69, - 70, - 73, - 32, - 34, - 32, - 102, - 114, - 111, - 109, - 32, - 80, - 99, - 105, - 82, - [111, 2], - 116, - 40, - 48, - 120, - 48, - 41, - 47, - 80, - 99, - 105, - 40, - 48, - 120, - 49, - 48, - 44, - 48, - 120, - 48, - 41, - 47, - 78, - 86, - 77, - 101, - 40, - 48, - 120, - 49, - 44, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 41, - 13, - 10, - 66, - 100, - 115, - 68, - 120, - 101, - 58, - 32, - 115, - 116, - 97, - 114, - 116, - 105, - 110, - 103, - 32, - 66, - [111, 2], - 116, - [48, 3], - 49, - 32, - 34, - 85, - 69, - 70, - 73, - 32, - 34, - 32, - 102, - 114, - 111, - 109, - 32, - 80, - 99, - 105, - 82, - [111, 2], - 116, - 40, - 48, - 120, - 48, - 41, - 47, - 80, - 99, - 105, - 40, - 48, - 120, - 49, - 48, - 44, - 48, - 120, - 48, - 41, - 47, - 78, - 86, - 77, - 101, - 40, - 48, - 120, - 49, - 44, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 45, - [48, 2], - 41, - 13, - 10, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 48, - 109, - 27, - 91, - 52, - 55, - 109, - 87, - 101, - 108, - 99, - 111, - 109, - 101, - 32, - 116, - 111, - 32, - 71, - 82, - 85, - 66, - 33, - 10, - 13, - 10, - 13, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 55, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 48, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 50, - 74, - 27, - 91, - 48, - 49, - 59, - 48, - 49, - 72, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 55, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 48, - 50, - 59, - 51, - 48, - 72, - 71, - 78, - 85, - 32, - 71, - 82, - 85, - 66, - [32, 2], - 118, - 101, - 114, - 115, - 105, - 111, - 110, - 32, - 50, - 46, - 48, - 54, - 10, - 13, - 10, - 13, - 27, - 91, - 48, - 52, - 59, - 48, - 50, - 72, - 218, - [196, 76], - 191, - 27, - 91, - 48, - 53, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 48, - 53, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 48, - 54, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 48, - 54, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 48, - 55, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 48, - 55, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 48, - 56, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 48, - 56, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 48, - 57, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 48, - 57, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 49, - 48, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 49, - 48, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - [49, 2], - 59, - 48, - 50, - 72, - 179, - 27, - 91, - [49, 2], - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 49, - 50, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 49, - 50, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 49, - 51, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 49, - 51, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 49, - 52, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 49, - 52, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 49, - 53, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 49, - 53, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 49, - 54, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 49, - 54, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 49, - 55, - 59, - 48, - 50, - 72, - 179, - 27, - 91, - 49, - 55, - 59, - 55, - 57, - 72, - 179, - 27, - 91, - 49, - 56, - 59, - 48, - 50, - 72, - 192, - [196, 76], - 217, - 27, - 91, - 49, - 57, - 59, - 48, - 50, - 72, - 27, - 91, - 50, - 48, - 59, - 48, - 50, - 72, - [32, 5], - 85, - 115, - 101, - 32, - 116, - 104, - 101, - 32, - 94, - 32, - 97, - 110, - 100, - 32, - 118, - 32, - 107, - 101, - 121, - 115, - 32, - 116, - 111, - 32, - 115, - 101, - 108, - 101, - 99, - 116, - 32, - 119, - 104, - 105, - 99, - 104, - 32, - 101, - 110, - 116, - 114, - 121, - 32, - 105, - 115, - 32, - 104, - 105, - 103, - 104, - 108, - 105, - 103, - 104, - 116, - 101, - 100, - 46, - [32, 10], - 10, - 13, - [32, 6], - 80, - 114, - 101, - [115, 2], - 32, - 101, - 110, - 116, - 101, - 114, - 32, - 116, - 111, - 32, - 98, - [111, 2], - 116, - 32, - 116, - 104, - 101, - 32, - 115, - 101, - 108, - 101, - 99, - 116, - 101, - 100, - 32, - 79, - 83, - 44, - 32, - 96, - 101, - 39, - 32, - 116, - 111, - 32, - 101, - 100, - 105, - 116, - 32, - 116, - 104, - 101, - 32, - 99, - 111, - [109, 2], - 97, - 110, - 100, - 115, - [32, 7], - 10, - 13, - [32, 6], - 98, - 101, - 102, - 111, - 114, - 101, - 32, - 98, - [111, 2], - 116, - 105, - 110, - 103, - 32, - 111, - 114, - 32, - 96, - 99, - 39, - 32, - 102, - 111, - 114, - 32, - 97, - 32, - 99, - 111, - [109, 2], - 97, - 110, - 100, - 45, - 108, - 105, - 110, - 101, - 46, - [32, 27], - 27, - 91, - 48, - 53, - 59, - 56, - 48, - 72, - 32, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 48, - 109, - 27, - 91, - 52, - 55, - 109, - 27, - 91, - 48, - 53, - 59, - 48, - 51, - 72, - 42, - 76, - 105, - 110, - 117, - 120, - 32, - 118, - 105, - 114, - 116, - [32, 65], - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 55, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 48, - 53, - 59, - 55, - 56, - 72, - 27, - 91, - 48, - 54, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 48, - 54, - 59, - 55, - 56, - 72, - 27, - 91, - 48, - 55, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 48, - 55, - 59, - 55, - 56, - 72, - 27, - 91, - 48, - 56, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 48, - 56, - 59, - 55, - 56, - 72, - 27, - 91, - 48, - 57, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 48, - 57, - 59, - 55, - 56, - 72, - 27, - 91, - 49, - 48, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 49, - 48, - 59, - 55, - 56, - 72, - 27, - 91, - [49, 2], - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - [49, 2], - 59, - 55, - 56, - 72, - 27, - 91, - 49, - 50, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 49, - 50, - 59, - 55, - 56, - 72, - 27, - 91, - 49, - 51, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 49, - 51, - 59, - 55, - 56, - 72, - 27, - 91, - 49, - 52, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 49, - 52, - 59, - 55, - 56, - 72, - 27, - 91, - 49, - 53, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 49, - 53, - 59, - 55, - 56, - 72, - 27, - 91, - 49, - 54, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 49, - 54, - 59, - 55, - 56, - 72, - 27, - 91, - 49, - 55, - 59, - 48, - 51, - 72, - [32, 76], - 27, - 91, - 49, - 55, - 59, - 55, - 56, - 72, - 27, - 91, - 49, - 55, - 59, - 56, - 48, - 72, - 32, - 27, - 91, - 48, - 53, - 59, - 55, - 56, - 72, - 27, - 91, - 50, - 51, - 59, - 48, - 49, - 72, - [32, 3], - 84, - 104, - 101, - 32, - 104, - 105, - 103, - 104, - 108, - 105, - 103, - 104, - 116, - 101, - 100, - 32, - 101, - 110, - 116, - 114, - 121, - 32, - 119, - 105, - [108, 2], - 32, - 98, - 101, - 32, - 101, - 120, - 101, - 99, - 117, - 116, - 101, - 100, - 32, - 97, - 117, - 116, - 111, - 109, - 97, - 116, - 105, - 99, - 97, - [108, 2], - 121, - 32, - 105, - 110, - 32, - 49, - 115, - 46, - [32, 17], - 27, - 91, - 48, - 53, - 59, - 55, - 56, - 72, - 27, - 91, - 50, - 51, - 59, - 48, - 49, - 72, - [32, 3], - 84, - 104, - 101, - 32, - 104, - 105, - 103, - 104, - 108, - 105, - 103, - 104, - 116, - 101, - 100, - 32, - 101, - 110, - 116, - 114, - 121, - 32, - 119, - 105, - [108, 2], - 32, - 98, - 101, - 32, - 101, - 120, - 101, - 99, - 117, - 116, - 101, - 100, - 32, - 97, - 117, - 116, - 111, - 109, - 97, - 116, - 105, - 99, - 97, - [108, 2], - 121, - 32, - 105, - 110, - 32, - 48, - 115, - 46, - [32, 17], - 27, - 91, - 48, - 53, - 59, - 55, - 56, - 72, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 48, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 50, - 74, - 27, - 91, - 48, - 49, - 59, - 48, - 49, - 72, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 55, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 48, - 109, - 27, - 91, - 52, - 48, - 109, - 27, - 91, - 50, - 74, - 27, - 91, - 48, - 49, - 59, - 48, - 49, - 72, - 27, - 91, - 48, - 109, - 27, - 91, - 51, - 55, - 109, - 27, - 91, - 52, - 48, - 109, - [32, 2], - 66, - [111, 2], - 116, - 105, - 110, - 103, - 32, - 96, - 76, - 105, - 110, - 117, - 120, - 32, - 118, - 105, - 114, - 116, - 39, - 10, - 13, - 10, - 13, - 27, - 55, - [32, 2], - 48, - 37, - [32, 45], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - [32, 2], - 54, - 37, - 32, - [35, 3], - [32, 41], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 49, - 53, - 37, - 32, - [35, 6], - [32, 38], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 49, - 54, - 37, - 32, - [35, 7], - [32, 37], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 49, - 57, - 37, - 32, - [35, 8], - [32, 36], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 48, - 37, - 32, - [35, 9], - [32, 35], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 49, - 37, - 32, - [35, 9], - [32, 35], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - [50, 2], - 37, - 32, - [35, 9], - [32, 35], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - [50, 2], - 37, - 32, - [35, 10], - [32, 34], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 51, - 37, - 32, - [35, 10], - [32, 34], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 52, - 37, - 32, - [35, 10], - [32, 34], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 53, - 37, - 32, - [35, 11], - [32, 33], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 54, - 37, - 32, - [35, 11], - [32, 33], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 55, - 37, - 32, - [35, 11], - [32, 33], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 55, - 37, - 32, - [35, 12], - [32, 32], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 56, - 37, - 32, - [35, 12], - [32, 32], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 57, - 37, - 32, - [35, 12], - [32, 32], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 50, - 57, - 37, - 32, - [35, 13], - [32, 31], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 48, - 37, - 32, - [35, 13], - [32, 31], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 49, - 37, - 32, - [35, 13], - [32, 31], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 49, - 37, - 32, - [35, 14], - [32, 30], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 50, - 37, - 32, - [35, 14], - [32, 30], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - [51, 2], - 37, - 32, - [35, 14], - [32, 30], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 52, - 37, - 32, - [35, 15], - [32, 29], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 53, - 37, - 32, - [35, 15], - [32, 29], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 54, - 37, - 32, - [35, 15], - [32, 29], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 54, - 37, - 32, - [35, 16], - [32, 28], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 55, - 37, - 32, - [35, 16], - [32, 28], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 56, - 37, - 32, - [35, 16], - [32, 28], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 56, - 37, - 32, - [35, 17], - [32, 27], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 51, - 57, - 37, - 32, - [35, 17], - [32, 27], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 48, - 37, - 32, - [35, 17], - [32, 27], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 48, - 37, - 32, - [35, 18], - [32, 26], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 49, - 37, - 32, - [35, 18], - [32, 26], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 50, - 37, - 32, - [35, 18], - [32, 26], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 51, - 37, - 32, - [35, 19], - [32, 25], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 53, - 37, - 32, - [35, 19], - [32, 25], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 53, - 37, - 32, - [35, 20], - [32, 24], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 54, - 37, - 32, - [35, 20], - [32, 24], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 55, - 37, - 32, - [35, 20], - [32, 24], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 55, - 37, - 32, - [35, 21], - [32, 23], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 52, - 57, - 37, - 32, - [35, 21], - [32, 23], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 53, - 48, - 37, - 32, - [35, 22], - [32, 22], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 55, - 49, - 37, - 32, - [35, 31], - [32, 13], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 55, - 54, - 37, - 32, - [35, 33], - [32, 11], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - [55, 2], - 37, - 32, - [35, 34], - [32, 10], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 56, - 50, - 37, - 32, - [35, 36], - [32, 8], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 56, - 51, - 37, - 32, - [35, 36], - [32, 8], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 56, - 52, - 37, - 32, - [35, 37], - [32, 7], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 56, - 54, - 37, - 32, - [35, 37], - [32, 7], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 56, - 55, - 37, - 32, - [35, 38], - [32, 6], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - [56, 2], - 37, - 32, - [35, 38], - [32, 6], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - [56, 2], - 37, - 32, - [35, 39], - [32, 5], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 56, - 57, - 37, - 32, - [35, 39], - [32, 5], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 57, - 48, - 37, - 32, - [35, 39], - [32, 5], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 57, - 48, - 37, - 32, - [35, 40], - [32, 4], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 57, - 49, - 37, - 32, - [35, 40], - [32, 4], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 57, - 50, - 37, - 32, - [35, 40], - [32, 4], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 57, - 51, - 37, - 32, - [35, 40], - [32, 4], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 32, - 57, - 51, - 37, - 32, - [35, 41], - [32, 3], - 27, - 56, - 27, - 91, - 48, - 75, - 27, - 55, - 49, - [48, 2], - 37, - 32, - [35, 44], - 27, - 56, - 27, - 91, - 48, - 75, - 13, - 10, - 13, - 10, - [32, 3], - 79, - 112, - 101, - 110, - 82, - 67, - 32, - 48, - 46, - [52, 2], - 46, - 49, - 48, - 32, - 105, - 115, - 32, - 115, - 116, - 97, - 114, - 116, - 105, - 110, - 103, - 32, - 117, - 112, - 32, - 76, - 105, - 110, - 117, - 120, - 32, - 53, - 46, - 49, - 53, - 46, - 52, - 49, - 45, - 48, - 45, - 118, - 105, - 114, - 116, - 32, - 40, - 120, - 56, - 54, - 95, - 54, - 52, - 41, - 13, - 10, - 13, - 10, - 32, - 42, - 32, - 47, - 112, - 114, - 111, - 99, - 32, - 105, - 115, - 32, - 97, - 108, - 114, - 101, - 97, - 100, - 121, - 32, - 109, - 111, - 117, - 110, - 116, - 101, - 100, - 13, - 10, - 32, - 42, - 32, - 77, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 47, - 114, - 117, - 110, - 32, - [46, 3], - 32, - 42, - 32, - 47, - 114, - 117, - 110, - 47, - 111, - 112, - 101, - 110, - 114, - 99, - 58, - 32, - 99, - 114, - 101, - 97, - 116, - 105, - 110, - 103, - 32, - 100, - 105, - 114, - 101, - 99, - 116, - 111, - 114, - 121, - 13, - 10, - 32, - 42, - 32, - 47, - 114, - 117, - 110, - 47, - 108, - 111, - 99, - 107, - 58, - 32, - 99, - 114, - 101, - 97, - 116, - 105, - 110, - 103, - 32, - 100, - 105, - 114, - 101, - 99, - 116, - 111, - 114, - 121, - 13, - 10, - 32, - 42, - 32, - 47, - 114, - 117, - 110, - 47, - 108, - 111, - 99, - 107, - 58, - 32, - 99, - 111, - [114, 2], - 101, - 99, - 116, - 105, - 110, - 103, - 32, - 111, - 119, - 110, - 101, - 114, - 13, - 10, - 32, - 42, - 32, - 67, - 97, - 99, - 104, - 105, - 110, - 103, - 32, - 115, - 101, - 114, - 118, - 105, - 99, - 101, - 32, - 100, - 101, - 112, - 101, - 110, - 100, - 101, - 110, - 99, - 105, - 101, - 115, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 82, - 101, - 109, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 100, - 101, - 118, - 116, - 109, - 112, - 102, - 115, - 32, - 111, - 110, - 32, - 47, - 100, - 101, - 118, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 77, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 47, - 100, - 101, - 118, - 47, - 109, - 113, - 117, - 101, - 117, - 101, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 77, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 109, - 111, - 100, - 108, - [111, 2], - 112, - [32, 2], - [46, 3], - 32, - 42, - 32, - 86, - 101, - 114, - 105, - 102, - 121, - 105, - 110, - 103, - 32, - 109, - 111, - 100, - 108, - [111, 2], - 112, - 13, - 10, - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 77, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 115, - 101, - 99, - 117, - 114, - 105, - 116, - 121, - 32, - 102, - 105, - 108, - 101, - 115, - 121, - 115, - 116, - 101, - 109, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 77, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 100, - 101, - 98, - 117, - 103, - 32, - 102, - 105, - 108, - 101, - 115, - 121, - 115, - 116, - 101, - 109, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 77, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 112, - 101, - 114, - 115, - 105, - 115, - 116, - 101, - 110, - 116, - 32, - 115, - 116, - 111, - 114, - 97, - 103, - 101, - 32, - 40, - 112, - 115, - 116, - 111, - 114, - 101, - 41, - 32, - 102, - 105, - 108, - 101, - 115, - 121, - 115, - 116, - 101, - 109, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 77, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 101, - 102, - 105, - 118, - 97, - 114, - 102, - 115, - 32, - 102, - 105, - 108, - 101, - 115, - 121, - 115, - 116, - 101, - 109, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 83, - 116, - 97, - 114, - 116, - 105, - 110, - 103, - 32, - 98, - 117, - 115, - 121, - 98, - 111, - 120, - 32, - 109, - 100, - 101, - 118, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 76, - 111, - 97, - 100, - 105, - 110, - 103, - 32, - 104, - 97, - 114, - 100, - 119, - 97, - 114, - 101, - 32, - 100, - 114, - 105, - 118, - 101, - 114, - 115, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 76, - 111, - 97, - 100, - 105, - 110, - 103, - 32, - 109, - 111, - 100, - 117, - 108, - 101, - 115, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 83, - 101, - [116, 2], - 105, - 110, - 103, - 32, - 115, - 121, - 115, - 116, - 101, - 109, - 32, - 99, - 108, - 111, - 99, - 107, - 32, - 117, - 115, - 105, - 110, - 103, - 32, - 116, - 104, - 101, - 32, - 104, - 97, - 114, - 100, - 119, - 97, - 114, - 101, - 32, - 99, - 108, - 111, - 99, - 107, - 32, - 91, - 85, - 84, - 67, - 93, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 67, - 104, - 101, - 99, - 107, - 105, - 110, - 103, - 32, - 108, - 111, - 99, - 97, - 108, - 32, - 102, - 105, - 108, - 101, - 115, - 121, - 115, - 116, - 101, - 109, - 115, - [32, 2], - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 82, - 101, - 109, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 102, - 105, - 108, - 101, - 115, - 121, - 115, - 116, - 101, - 109, - 115, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 77, - 111, - 117, - 110, - 116, - 105, - 110, - 103, - 32, - 108, - 111, - 99, - 97, - 108, - 32, - 102, - 105, - 108, - 101, - 115, - 121, - 115, - 116, - 101, - 109, - 115, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 67, - 111, - 110, - 102, - 105, - 103, - 117, - 114, - 105, - 110, - 103, - 32, - 107, - 101, - 114, - 110, - 101, - 108, - 32, - 112, - 97, - 114, - 97, - 109, - 101, - 116, - 101, - 114, - 115, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 77, - 105, - 103, - 114, - 97, - 116, - 105, - 110, - 103, - 32, - 47, - 118, - 97, - 114, - 47, - 108, - 111, - 99, - 107, - 32, - 116, - 111, - 32, - 47, - 114, - 117, - 110, - 47, - 108, - 111, - 99, - 107, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 67, - 114, - 101, - 97, - 116, - 105, - 110, - 103, - 32, - 117, - 115, - 101, - 114, - 32, - 108, - 111, - 103, - 105, - 110, - 32, - 114, - 101, - 99, - 111, - 114, - 100, - 115, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 67, - 108, - 101, - 97, - 110, - 105, - 110, - 103, - 32, - 47, - 116, - 109, - 112, - 32, - 100, - 105, - 114, - 101, - 99, - 116, - 111, - 114, - 121, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 83, - 101, - [116, 2], - 105, - 110, - 103, - 32, - 104, - 111, - 115, - 116, - 110, - 97, - 109, - 101, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 83, - 116, - 97, - 114, - 116, - 105, - 110, - 103, - 32, - 98, - 117, - 115, - 121, - 98, - 111, - 120, - 32, - 115, - 121, - 115, - 108, - 111, - 103, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - 32, - 42, - 32, - 83, - 116, - 97, - 114, - 116, - 105, - 110, - 103, - 32, - 102, - 105, - 114, - 115, - 116, - 98, - [111, 2], - 116, - 32, - [46, 3], - 32, - 91, - 32, - 111, - 107, - 32, - 93, - 13, - 10, - [13, 2], - 10, - 87, - 101, - 108, - 99, - 111, - 109, - 101, - 32, - 116, - 111, - 32, - 65, - 108, - 112, - 105, - 110, - 101, - 32, - 76, - 105, - 110, - 117, - 120, - 32, - 51, - 46, - 49, - 54, - 13, - 10, - 13, - 75, - 101, - 114, - 110, - 101, - 108, - 32, - 53, - 46, - 49, - 53, - 46, - 52, - 49, - 45, - 48, - 45, - 118, - 105, - 114, - 116, - 32, - 111, - 110, - 32, - 97, - 110, - 32, - 120, - 56, - 54, - 95, - 54, - 52, - 32, - 40, - 47, - 100, - 101, - 118, - 47, - [116, 2], - 121, - 83, - 48, - 41, - 13, - 10, - [13, 2], - 10, - 13, - 108, - 111, - 99, - 97, - 108, - 104, - 111, - 115, - 116, - 32, - 108, - 111, - 103, - 105, - 110, - 58, - 32, - ].flatMap(function (v) { return (Array.isArray(v) ? Array(v[1]).fill(v[0]) : v); }), -}; diff --git a/libs/api-mocks/silo.js b/libs/api-mocks/silo.js deleted file mode 100644 index e09de742e8..0000000000 --- a/libs/api-mocks/silo.js +++ /dev/null @@ -1,96 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.toIdp = exports.identityProviders = exports.samlIdp = exports.siloProvisioned = exports.siloQuotas = exports.defaultSilo = exports.silos = void 0; -var util_1 = require("@oxide/util"); -exports.silos = [ - { - id: '6d3a9c06-475e-4f75-b272-c0d0e3f980fa', - name: 'maze-war', - description: 'a silo', - time_created: new Date(2021, 3, 1).toISOString(), - time_modified: new Date(2021, 4, 2).toISOString(), - discoverable: true, - identity_mode: 'saml_jit', - mapped_fleet_roles: { - admin: ['admin'], - }, - }, - { - id: '68b58556-15b9-4ccb-adff-9fd3c7de1f9a', - name: 'myriad', - description: 'a second silo', - time_created: new Date(2023, 1, 28).toISOString(), - time_modified: new Date(2023, 6, 12).toISOString(), - discoverable: true, - identity_mode: 'saml_jit', - mapped_fleet_roles: {}, - }, -]; -exports.defaultSilo = exports.silos[0]; -exports.siloQuotas = [ - { - silo_id: exports.silos[0].id, - cpus: 50, - memory: 300 * util_1.GiB, - storage: 7 * util_1.TiB, - }, - { - silo_id: exports.silos[1].id, - cpus: 34, - memory: 500 * util_1.GiB, - storage: 9 * util_1.TiB, - }, -]; -// unlike siloQuotas, this doesn't exactly match how it's done in Nexus, but -// it's good enough. All we need is to be able to pull the provisioned amounts -// for a given silo. Note it has the same shape as the quotas object. -exports.siloProvisioned = [ - { - silo_id: exports.silos[0].id, - cpus: 30, - memory: 234 * util_1.GiB, - storage: 4.3 * util_1.TiB, - }, - { - silo_id: exports.silos[1].id, - cpus: 8, - memory: 150 * util_1.GiB, - storage: 2 * util_1.TiB, - }, -]; -exports.samlIdp = { - id: '2a96ce6f-c178-4631-9cde-607d65b539c7', - description: 'An identity provider but what if it had a really long description', - name: 'mock-idp', - time_created: new Date(2021, 4, 3, 4).toISOString(), - time_modified: new Date(2021, 4, 3, 5).toISOString(), - acs_url: '', - idp_entity_id: '', - public_cert: '', - slo_url: '', - sp_client_id: '', - technical_contact_email: '', -}; -exports.identityProviders = [ - { type: 'saml', siloId: exports.defaultSilo.id, provider: exports.samlIdp }, -]; -/** - * Extract generic `IdentityProvider` from a specific `*IdentityProvider` - * type like `SamlIdentityProvider` - */ -var toIdp = function (_a) { - var provider = _a.provider, type = _a.type; - return (__assign({ provider_type: type }, (0, util_1.pick)(provider, 'id', 'name', 'description', 'time_created', 'time_modified'))); -}; -exports.toIdp = toIdp; diff --git a/libs/api-mocks/sled.js b/libs/api-mocks/sled.js deleted file mode 100644 index 4909c48df4..0000000000 --- a/libs/api-mocks/sled.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sleds = exports.sled = void 0; -exports.sled = { - id: 'c2519937-44a4-493b-9b38-5c337c597d08', - time_created: new Date(2021, 0, 1).toISOString(), - time_modified: new Date(2021, 0, 2).toISOString(), - rack_id: '6fbafcc7-1626-4785-be65-e212f8ad66d0', - provision_state: 'provisionable', - baseboard: { - part: '913-0000008', - serial: 'BRM02222867', - revision: 0, - }, - usable_hardware_threads: 128, - usable_physical_ram: 1099511627776, -}; -exports.sleds = [exports.sled]; diff --git a/libs/api-mocks/snapshot.js b/libs/api-mocks/snapshot.js deleted file mode 100644 index 2ffcaef968..0000000000 --- a/libs/api-mocks/snapshot.js +++ /dev/null @@ -1,95 +0,0 @@ -"use strict"; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.snapshots = void 0; -var uuid_1 = require("uuid"); -var disk_1 = require("./disk"); -var project_1 = require("./project"); -var generatedSnapshots = Array.from({ length: 25 }, function (_, i) { - return generateSnapshot(i); -}); -exports.snapshots = __spreadArray([ - { - id: 'ab805e59-b6b8-4c73-8081-6a224b6b0698', - name: 'snapshot-1', - description: "it's a snapshot", - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - size: 1024, - disk_id: disk_1.disks[0].id, - state: 'ready', - }, - { - id: '9a29813d-e94b-4c6a-82a0-672af3f78a6f', - name: 'snapshot-2', - description: "it's a second snapshot", - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - size: 2048, - disk_id: disk_1.disks[0].id, - state: 'ready', - }, - { - id: 'e6c58826-62fb-4205-820e-620407cd04e7', - name: 'delete-500', - description: "it's a third snapshot", - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - size: 3072, - disk_id: disk_1.disks[0].id, - state: 'ready', - }, - { - id: 'dc598369-4554-4ccd-aa89-a837e6ca487d', - name: 'snapshot-4', - description: "it's a fourth snapshot", - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - size: 4096, - disk_id: disk_1.disks[0].id, - state: 'ready', - }, - { - id: 'ca117fc6-d3e4-452e-9e1c-15abea752ff6', - name: 'snapshot-disk-deleted', - description: 'technically it never existed', - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - size: 5120, - disk_id: 'a6f61e3f-25c1-49b0-a013-ac6a2d98a948', - state: 'ready', - } -], generatedSnapshots, true); -function generateSnapshot(index) { - return { - id: (0, uuid_1.v4)(), - name: "disk-1-snapshot-".concat(index + 5), - description: '', - project_id: project_1.project.id, - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - size: 1024 * (index + 1), - disk_id: disk_1.disks[0].id, - state: 'ready', - }; -} diff --git a/libs/api-mocks/sshKeys.js b/libs/api-mocks/sshKeys.js deleted file mode 100644 index d52b6b8bbf..0000000000 --- a/libs/api-mocks/sshKeys.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sshKeys = void 0; -var user_1 = require("./user"); -exports.sshKeys = [ - { - id: '43af8bc5-6f8e-404d-8b39-72b07cc9da56', - name: 'm1-macbook-pro', - description: 'For use on personal projects', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - public_key: 'aslsddlfkjsdlfkjsdlfkjsdlfkjsdflkjsdlfkjsdlfkjsd', - silo_user_id: user_1.user1.id, - }, - { - id: 'b2c3d4e5-6f7g-8h9i-0j1k-2l3m4n5o6p7q', - name: 'mac-mini', - description: '', - time_created: new Date().toISOString(), - time_modified: new Date().toISOString(), - public_key: 'aslsddlfkjsdlfkjsdlfkjsdlfkjsdflkjsdlfkjsdlfkjsd', - silo_user_id: user_1.user1.id, - }, -]; diff --git a/libs/api-mocks/user-group.js b/libs/api-mocks/user-group.js deleted file mode 100644 index e333e4a937..0000000000 --- a/libs/api-mocks/user-group.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.groupMemberships = exports.userGroups = exports.userGroup3 = exports.userGroup2 = exports.userGroup1 = void 0; -var silo_1 = require("./silo"); -var user_1 = require("./user"); -exports.userGroup1 = { - id: '0ff6da96-5d6d-4326-b059-2b72c1b51457', - silo_id: silo_1.defaultSilo.id, - display_name: 'web-devs', -}; -exports.userGroup2 = { - id: '1b5fa004-a378-4225-960f-60f089684b05', - silo_id: silo_1.defaultSilo.id, - display_name: 'kernel-devs', -}; -exports.userGroup3 = { - id: '5e30797c-cae3-4402-aeb7-d5044c4bed29', - silo_id: silo_1.defaultSilo.id, - display_name: 'real-estate-devs', -}; -exports.userGroups = [exports.userGroup1, exports.userGroup2, exports.userGroup3]; -exports.groupMemberships = [ - { - userId: user_1.user1.id, - groupId: exports.userGroup1.id, - }, - { - userId: user_1.user2.id, - groupId: exports.userGroup3.id, - }, -]; diff --git a/libs/api-mocks/user.js b/libs/api-mocks/user.js deleted file mode 100644 index 919c3fcb79..0000000000 --- a/libs/api-mocks/user.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.users = exports.user4 = exports.user3 = exports.user2 = exports.user1 = void 0; -var silo_1 = require("./silo"); -exports.user1 = { - id: '2e28576d-43e0-4e9e-9132-838a7b66f602', - display_name: 'Hannah Arendt', - silo_id: silo_1.defaultSilo.id, -}; -exports.user2 = { - id: '6937b251-013c-4f96-9afc-c62b1318dd0b', - display_name: 'Hans Jonas', - silo_id: silo_1.defaultSilo.id, -}; -exports.user3 = { - id: '4962021b-35e1-44a7-a40c-2264cd540615', - display_name: 'Jacob Klein', - silo_id: silo_1.defaultSilo.id, -}; -exports.user4 = { - id: '37c6aa2f-899e-4d56-bad1-93b5526a7151', - display_name: 'Simone de Beauvoir', - silo_id: silo_1.defaultSilo.id, -}; -exports.users = [exports.user1, exports.user2, exports.user3, exports.user4]; diff --git a/libs/api-mocks/vpc.js b/libs/api-mocks/vpc.js deleted file mode 100644 index 8f9227ec4f..0000000000 --- a/libs/api-mocks/vpc.js +++ /dev/null @@ -1,117 +0,0 @@ -"use strict"; -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.defaultFirewallRules = exports.vpcSubnet2 = exports.vpcSubnet = exports.vpc = void 0; -var project_1 = require("./project"); -var time_created = new Date(2021, 0, 1).toISOString(); -var time_modified = new Date(2021, 0, 2).toISOString(); -var systemRouterId = 'b5af837b-b986-4a0a-b775-516d76c84ec3'; -exports.vpc = { - id: '87774ff3-c6c1-475b-b920-ba2954f390fe', - name: 'mock-vpc', - description: 'a fake vpc', - dns_name: 'mock-vpc', - project_id: project_1.project.id, - system_router_id: systemRouterId, - ipv6_prefix: 'fdf6:1818:b6e1::/48', - time_created: time_created, - time_modified: time_modified, -}; -exports.vpcSubnet = { - // this is supposed to be flattened into the top level. will fix in API - id: 'd12bf934-d2bf-40e9-8596-bb42a7793749', - name: 'mock-subnet', - description: 'a fake subnet', - time_created: new Date(2021, 0, 1).toISOString(), - time_modified: new Date(2021, 0, 2).toISOString(), - // supposed to be camelcase, will fix in API - vpc_id: exports.vpc.id, - ipv4_block: '10.1.1.1/24', - ipv6_block: 'fd9b:870a:4245::/64', -}; -exports.vpcSubnet2 = __assign(__assign({}, exports.vpcSubnet), { id: 'cb001986-1dbe-440c-8872-a769a5c3cda6', name: 'mock-subnet-2', vpc_id: exports.vpc.id, ipv4_block: '10.1.1.2/24' }); -exports.defaultFirewallRules = [ - { - id: 'b74aeea8-1201-4efd-b6ec-011f10a0b176', - name: 'allow-internal-inbound', - status: 'enabled', - direction: 'inbound', - targets: [{ type: 'vpc', value: 'default' }], - action: 'allow', - description: 'allow inbound traffic to all instances within the VPC if originated within the VPC', - filters: { - hosts: [{ type: 'vpc', value: 'default' }], - }, - priority: 65534, - time_created: time_created, - time_modified: time_modified, - vpc_id: exports.vpc.id, - }, - { - id: '9802cd8e-1e59-4fdf-9b40-99c189f7a19b', - name: 'allow-ssh', - status: 'enabled', - direction: 'inbound', - targets: [{ type: 'vpc', value: 'default' }], - description: 'allow inbound TCP connections on port 22 from anywhere', - filters: { - ports: ['22'], - protocols: ['TCP'], - }, - action: 'allow', - priority: 65534, - time_created: time_created, - time_modified: time_modified, - vpc_id: exports.vpc.id, - }, - { - id: 'cde07d86-b8c0-49ed-8754-55f1bdee20fe', - name: 'allow-icmp', - status: 'enabled', - direction: 'inbound', - targets: [{ type: 'vpc', value: 'default' }], - description: 'allow inbound ICMP traffic from anywhere', - filters: { - protocols: ['ICMP'], - }, - action: 'allow', - priority: 65534, - time_created: time_created, - time_modified: time_modified, - vpc_id: exports.vpc.id, - }, - { - id: '5ed562d9-2566-496d-b7b3-7976b04a0b80', - name: 'allow-rdp', - status: 'enabled', - direction: 'inbound', - targets: [{ type: 'vpc', value: 'default' }], - description: 'allow inbound TCP connections on port 3389 from anywhere', - filters: { - ports: ['3389'], - protocols: ['TCP'], - }, - action: 'allow', - priority: 65534, - time_created: time_created, - time_modified: time_modified, - vpc_id: exports.vpc.id, - }, -]; From 0504fb395f94bc2142eab70ac88d88d12c6d7693 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 14 Feb 2024 16:53:00 -0800 Subject: [PATCH 29/64] Refactor deletion flow --- .../project/networking/FloatingIpsTab.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/pages/project/networking/FloatingIpsTab.tsx b/app/pages/project/networking/FloatingIpsTab.tsx index cddf5cc21e..970c64e967 100644 --- a/app/pages/project/networking/FloatingIpsTab.tsx +++ b/app/pages/project/networking/FloatingIpsTab.tsx @@ -93,17 +93,14 @@ export function FloatingIpsTab() { disabled: isAttachedToAnInstance ? 'This floating IP must be detached from the instance before it can be deleted' : false, - onActivate: () => { - confirmDelete({ - doDelete: () => - deleteFloatingIp.mutateAsync({ - path: { floatingIp: floatingIp.name }, - query: { project }, - }), - label: floatingIp.name, - }), - setFloatingIpToModify(null) - }, + onActivate: confirmDelete({ + doDelete: () => + deleteFloatingIp.mutateAsync({ + path: { floatingIp: floatingIp.name }, + query: { project }, + }), + label: floatingIp.name, + }), }, ] } From 999c3ac31d4c1d132f429fc8f5f4577e81ed0b7d Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 14 Feb 2024 17:06:18 -0800 Subject: [PATCH 30/64] Refactor form --- app/forms/floating-ip-create.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index b23145a63f..abc103d5b5 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -140,9 +140,8 @@ export function CreateFloatingIpSideModalForm({ {allPools && ( toListboxItem(p))} + items={allPools.items.map((p) => toListboxItem(p))} label="Pool" - required control={form.control} /> )} From b57a539527894910dc206610534ab00bb96ae56e Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 15 Feb 2024 08:45:36 -0800 Subject: [PATCH 31/64] Fix import to align with new rule --- libs/api-mocks/floating-ip.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/api-mocks/floating-ip.ts b/libs/api-mocks/floating-ip.ts index 08c3ca5ff8..5a0456bfbd 100644 --- a/libs/api-mocks/floating-ip.ts +++ b/libs/api-mocks/floating-ip.ts @@ -8,8 +8,9 @@ import type { FloatingIp } from '@oxide/api' -import { instance, project } from '.' +import { instance } from './instance' import type { Json } from './json-type' +import { project } from './project' // A floating IP from the default pool export const floatingIp: Json = { From 2dc8bd78c0b9f5f4173bad637aa5283812e861a0 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 15 Feb 2024 15:10:23 -0800 Subject: [PATCH 32/64] Moving Floating IPs to project resource level --- app/layouts/ProjectLayout.tsx | 9 +- .../project/floating-ips/FloatingIpsPage.tsx | 258 ++++++++++++++++++ app/pages/project/floating-ips/index.ts | 9 + app/pages/project/index.tsx | 3 +- .../networking/{VpcsTab.tsx => VpcsPage.tsx} | 20 +- app/pages/project/networking/index.tsx | 2 +- .../{networking => vpcs}/FloatingIpsTab.tsx | 3 +- app/pages/project/vpcs/VpcPage/VpcPage.tsx | 77 ++++++ app/pages/project/vpcs/VpcPage/index.tsx | 9 + .../vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx | 155 +++++++++++ .../vpcs/VpcPage/tabs/VpcGatewaysTab.tsx | 7 + .../vpcs/VpcPage/tabs/VpcSubnetsTab.tsx | 78 ++++++ app/pages/project/vpcs/VpcsPage.tsx | 117 ++++++++ app/pages/project/vpcs/index.tsx | 10 + app/routes.tsx | 72 +++-- app/util/path-builder.ts | 8 +- 16 files changed, 785 insertions(+), 52 deletions(-) create mode 100644 app/pages/project/floating-ips/FloatingIpsPage.tsx create mode 100644 app/pages/project/floating-ips/index.ts rename app/pages/project/networking/{VpcsTab.tsx => VpcsPage.tsx} (89%) rename app/pages/project/{networking => vpcs}/FloatingIpsTab.tsx (98%) create mode 100644 app/pages/project/vpcs/VpcPage/VpcPage.tsx create mode 100644 app/pages/project/vpcs/VpcPage/index.tsx create mode 100644 app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx create mode 100644 app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx create mode 100644 app/pages/project/vpcs/VpcPage/tabs/VpcSubnetsTab.tsx create mode 100644 app/pages/project/vpcs/VpcsPage.tsx create mode 100644 app/pages/project/vpcs/index.tsx diff --git a/app/layouts/ProjectLayout.tsx b/app/layouts/ProjectLayout.tsx index 046ff29f22..8faa04ed0a 100644 --- a/app/layouts/ProjectLayout.tsx +++ b/app/layouts/ProjectLayout.tsx @@ -20,6 +20,7 @@ import { Folder16Icon, Images16Icon, Instances16Icon, + IpGlobal16Icon, Networking16Icon, Snapshots16Icon, Storage16Icon, @@ -67,7 +68,8 @@ function ProjectLayout({ overrideContentPane }: ProjectLayoutProps) { { value: 'Disks', path: pb.disks(projectSelector) }, { value: 'Snapshots', path: pb.snapshots(projectSelector) }, { value: 'Images', path: pb.projectImages(projectSelector) }, - { value: 'Networking', path: pb.vpcs(projectSelector) }, + { value: 'VPCs', path: pb.vpcs(projectSelector) }, + { value: 'Floating IPs', path: pb.floatingIps(projectSelector) }, { value: 'Access & IAM', path: pb.projectAccess(projectSelector) }, ] // filter out the entry for the path we're currently on @@ -111,7 +113,10 @@ function ProjectLayout({ overrideContentPane }: ProjectLayoutProps) { Images - Networking + VPCs + + + Floating IPs Access & IAM diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx new file mode 100644 index 0000000000..9c40afd121 --- /dev/null +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -0,0 +1,258 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom' + +import { + apiQueryClient, + useApiMutation, + useApiQueryClient, + usePrefetchedApiQuery, + type FloatingIp, + type Instance, +} from '@oxide/api' +import { useQueryTable, type MenuAction } from '@oxide/table' +import { + buttonStyle, + EmptyMessage, + IpGlobal24Icon, + Listbox, + Modal, + Networking24Icon, + PageHeader, + PageTitle, + TableActions, +} from '@oxide/ui' + +import { getProjectSelector, useProjectSelector } from 'app/hooks' +import { confirmDelete } from 'app/stores/confirm-delete' +import { addToast } from 'app/stores/toast' +import { pb } from 'app/util/path-builder' + +const EmptyState = () => ( + } + title="No Floating IPs" + body="You need to create a Floating IP to be able to see it here" + buttonText="New Floating IP" + buttonTo={pb.floatingIpNew(useProjectSelector())} + /> +) + +FloatingIpsPage.loader = async ({ params }: LoaderFunctionArgs) => { + const { project } = getProjectSelector(params) + await Promise.all([ + apiQueryClient.prefetchQuery('floatingIpList', { + query: { project, limit: 25 }, + }), + apiQueryClient.prefetchQuery('instanceList', { + query: { project }, + }), + ]) + return null +} + +export function FloatingIpsPage() { + const [attachModalOpen, setAttachModalOpen] = useState(false) + const [detachModalOpen, setDetachModalOpen] = useState(false) + const [floatingIpToModify, setFloatingIpToModify] = useState(null) + const queryClient = useApiQueryClient() + const { project } = useProjectSelector() + const { data: instances } = usePrefetchedApiQuery('instanceList', { + query: { project }, + }) + + const deleteFloatingIp = useApiMutation('floatingIpDelete', { + onSuccess() { + queryClient.invalidateQueries('floatingIpList') + addToast({ content: 'Your Floating IP has been deleted' }) + }, + }) + + const makeActions = (floatingIp: FloatingIp): MenuAction[] => { + const isAttachedToAnInstance = !!floatingIp.instanceId + return [ + { + label: 'Attach', + disabled: isAttachedToAnInstance + ? 'This floating IP must be detached from the existing instance before it can be attached to a new one' + : false, + onActivate() { + setFloatingIpToModify(floatingIp) + setAttachModalOpen(true) + }, + }, + { + label: 'Detach', + disabled: isAttachedToAnInstance + ? false + : 'This floating IP is not attached to an instance', + onActivate() { + setFloatingIpToModify(floatingIp) + setDetachModalOpen(true) + }, + }, + { + label: 'Delete', + disabled: isAttachedToAnInstance + ? 'This floating IP must be detached from the instance before it can be deleted' + : false, + onActivate: confirmDelete({ + doDelete: () => + deleteFloatingIp.mutateAsync({ + path: { floatingIp: floatingIp.name }, + query: { project }, + }), + label: floatingIp.name, + }), + }, + ] + } + + const getInstanceName = (instanceId: string) => + instances.items.find((i) => i.id === instanceId)?.name + + const { Table, Column } = useQueryTable('floatingIpList', { query: { project } }) + return ( + <> + + }>Floating IPs + + + + New Floating IP + + + } makeActions={makeActions}> + + + + getInstanceName(instanceId)} + /> +
+ + {attachModalOpen && floatingIpToModify && ( + setAttachModalOpen(false)} + /> + )} + {detachModalOpen && floatingIpToModify?.instanceId && ( + setDetachModalOpen(false)} + /> + )} + + ) +} + +const AttachFloatingIpModal = ({ + floatingIp, + instances, + project, + onDismiss, +}: { + floatingIp: string + instances: Array + project: string + onDismiss: () => void +}) => { + const queryClient = useApiQueryClient() + const floatingIpAttach = useApiMutation('floatingIpAttach', { + onSuccess() { + queryClient.invalidateQueries('floatingIpList') + addToast({ content: 'Your Floating IP has been attached' }) + onDismiss() + }, + onError: (err) => { + addToast({ title: 'Error', content: err.message, variant: 'error' }) + }, + }) + const form = useForm({ defaultValues: { instanceId: '' } }) + + return ( + + + +
+ ({ value: i.id, label: i.name }))} + label="Select an instance" + onChange={(e) => { + form.setValue('instanceId', e) + }} + selected={form.watch('instanceId')} + /> + +
+
+ + floatingIpAttach.mutate({ + path: { floatingIp }, + query: { project }, + body: { kind: 'instance', parent: form.getValues('instanceId') }, + }) + } + onDismiss={onDismiss} + > +
+ ) +} + +const DetachFloatingIpModal = ({ + floatingIp, + instance, + project, + onDismiss, +}: { + floatingIp: string + instance: string + project: string + onDismiss: () => void +}) => { + const queryClient = useApiQueryClient() + const floatingIpDetach = useApiMutation('floatingIpDetach', { + onSuccess() { + queryClient.invalidateQueries('floatingIpList') + addToast({ content: 'Your Floating IP has been detached' }) + onDismiss() + }, + onError: (err) => { + addToast({ title: 'Error', content: err.message, variant: 'error' }) + }, + }) + return ( + + + + Detach {floatingIp} from {instance}? + + + + floatingIpDetach.mutate({ path: { floatingIp }, query: { project } }) + } + onDismiss={onDismiss} + > + + ) +} diff --git a/app/pages/project/floating-ips/index.ts b/app/pages/project/floating-ips/index.ts new file mode 100644 index 0000000000..522c5a7cbd --- /dev/null +++ b/app/pages/project/floating-ips/index.ts @@ -0,0 +1,9 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +export * from './FloatingIpsPage' diff --git a/app/pages/project/index.tsx b/app/pages/project/index.tsx index 1a77061b66..73d86cb57f 100644 --- a/app/pages/project/index.tsx +++ b/app/pages/project/index.tsx @@ -10,5 +10,6 @@ export * from './access' export * from './disks' export * from './images' export * from './instances' -export * from './networking' +export * from './vpcs' +export * from './floating-ips' export * from './snapshots' diff --git a/app/pages/project/networking/VpcsTab.tsx b/app/pages/project/networking/VpcsPage.tsx similarity index 89% rename from app/pages/project/networking/VpcsTab.tsx rename to app/pages/project/networking/VpcsPage.tsx index ced58ebb13..286faa08f3 100644 --- a/app/pages/project/networking/VpcsTab.tsx +++ b/app/pages/project/networking/VpcsPage.tsx @@ -16,7 +16,14 @@ import { type Vpc, } from '@oxide/api' import { DateCell, linkCell, useQueryTable, type MenuAction } from '@oxide/table' -import { buttonStyle, EmptyMessage, Networking24Icon } from '@oxide/ui' +import { + buttonStyle, + EmptyMessage, + Networking24Icon, + PageHeader, + PageTitle, + TableActions, +} from '@oxide/ui' import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' import { confirmDelete } from 'app/stores/confirm-delete' @@ -34,14 +41,14 @@ const EmptyState = () => ( // just as in the vpcList call for the quick actions menu, include limit: 25 to make // sure it matches the call in the QueryTable -VpcsTab.loader = async ({ params }: LoaderFunctionArgs) => { +VpcsPage.loader = async ({ params }: LoaderFunctionArgs) => { await apiQueryClient.prefetchQuery('vpcList', { query: { ...getProjectSelector(params), limit: 25 }, }) return null } -export function VpcsTab() { +export function VpcsPage() { const queryClient = useApiQueryClient() const projectSelector = useProjectSelector() const { data: vpcs } = usePrefetchedApiQuery('vpcList', { @@ -87,11 +94,14 @@ export function VpcsTab() { const { Table, Column } = useQueryTable('vpcList', { query: projectSelector }) return ( <> -
+ + }>VPCs + + New Vpc -
+
} makeActions={makeActions}>
- New Floating IP + New Floating IPasdasdasdadasdasdasdaddsafsda as dsad sdasdsad asdasdd ad asdasd + asdasd asdasd asd sa
} makeActions={makeActions}> diff --git a/app/pages/project/vpcs/VpcPage/VpcPage.tsx b/app/pages/project/vpcs/VpcPage/VpcPage.tsx new file mode 100644 index 0000000000..6ab708ed5b --- /dev/null +++ b/app/pages/project/vpcs/VpcPage/VpcPage.tsx @@ -0,0 +1,77 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import type { LoaderFunctionArgs } from 'react-router-dom' + +import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' +import { Networking24Icon, PageHeader, PageTitle, PropertiesTable, Tabs } from '@oxide/ui' +import { formatDateTime } from '@oxide/util' + +import { QueryParamTabs } from 'app/components/QueryParamTabs' +import { getVpcSelector, useVpcSelector } from 'app/hooks' + +import { VpcFirewallRulesTab } from './tabs/VpcFirewallRulesTab' +import { VpcSubnetsTab } from './tabs/VpcSubnetsTab' + +VpcPage.loader = async ({ params }: LoaderFunctionArgs) => { + const { project, vpc } = getVpcSelector(params) + await Promise.all([ + apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } }), + apiQueryClient.prefetchQuery('vpcFirewallRulesView', { + query: { project, vpc }, + }), + apiQueryClient.prefetchQuery('vpcSubnetList', { + query: { project, vpc, limit: 25 }, + }), + ]) + return null +} + +export function VpcPage() { + const { project, vpc: vpcName } = useVpcSelector() + const { data: vpc } = usePrefetchedApiQuery('vpcView', { + path: { vpc: vpcName }, + query: { project }, + }) + + return ( + <> + + }> + {vpc.name} adasdasdasdasdasdasdasdadasdasdasd + + + + + {vpc.description} + {vpc.dnsName} + + + + {formatDateTime(vpc.timeCreated)} + + + {formatDateTime(vpc.timeModified)} + + + + + + + Subnets + Firewall Rules + + + + + + + + + + ) +} diff --git a/app/pages/project/vpcs/VpcPage/index.tsx b/app/pages/project/vpcs/VpcPage/index.tsx new file mode 100644 index 0000000000..c49ee5e598 --- /dev/null +++ b/app/pages/project/vpcs/VpcPage/index.tsx @@ -0,0 +1,9 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +export * from './VpcPage' diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx new file mode 100644 index 0000000000..c955905662 --- /dev/null +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx @@ -0,0 +1,155 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { useMemo, useState } from 'react' + +import { + useApiMutation, + useApiQueryClient, + usePrefetchedApiQuery, + type VpcFirewallRule, +} from '@oxide/api' +import { + ButtonCell, + createColumnHelper, + DateCell, + EnabledCell, + FirewallFilterCell, + getActionsCol, + Table, + TypeValueListCell, + useReactTable, +} from '@oxide/table' +import { Button, EmptyMessage, TableEmptyBox } from '@oxide/ui' +import { sortBy, titleCase } from '@oxide/util' + +import { CreateFirewallRuleForm } from 'app/forms/firewall-rules-create' +import { EditFirewallRuleForm } from 'app/forms/firewall-rules-edit' +import { useVpcSelector } from 'app/hooks' +import { confirmDelete } from 'app/stores/confirm-delete' + +const colHelper = createColumnHelper() + +/** columns that don't depend on anything in `render` */ +const staticColumns = [ + colHelper.accessor('priority', { + header: 'Priority', + cell: (info) =>
{info.getValue()}
, + }), + colHelper.accessor('action', { + header: 'Action', + cell: (info) =>
{titleCase(info.getValue())}
, + }), + colHelper.accessor('direction', { + header: 'Direction', + cell: (info) =>
{titleCase(info.getValue())}
, + }), + colHelper.accessor('targets', { + header: 'Targets', + cell: (info) => , + }), + colHelper.accessor('filters', { + header: 'Filters', + cell: (info) => , + }), + colHelper.accessor('status', { + header: 'Status', + cell: (info) => , + }), + colHelper.accessor('timeCreated', { + id: 'created', + header: 'Created', + cell: (info) => , + }), +] + +export const VpcFirewallRulesTab = () => { + const queryClient = useApiQueryClient() + const vpcSelector = useVpcSelector() + + const { data } = usePrefetchedApiQuery('vpcFirewallRulesView', { + query: vpcSelector, + }) + const rules = useMemo(() => sortBy(data.rules, (r) => r.priority), [data]) + + const [createModalOpen, setCreateModalOpen] = useState(false) + const [editing, setEditing] = useState(null) + + const updateRules = useApiMutation('vpcFirewallRulesUpdate', { + onSuccess() { + queryClient.invalidateQueries('vpcFirewallRulesView') + }, + }) + + // the whole thing can't be static because the action depends on setEditing + const columns = useMemo(() => { + return [ + colHelper.accessor('name', { + header: 'Name', + cell: (info) => ( + setEditing(info.row.original)}> + {info.getValue()} + + ), + }), + ...staticColumns, + getActionsCol((rule: VpcFirewallRule) => [ + { label: 'Edit', onActivate: () => setEditing(rule) }, + { + label: 'Delete', + onActivate: confirmDelete({ + doDelete: () => + updateRules.mutateAsync({ + query: vpcSelector, + body: { + rules: rules.filter((r) => r.id !== rule.id), + }, + }), + label: rule.name, + }), + }, + ]), + ] + }, [setEditing, rules, updateRules, vpcSelector]) + + const table = useReactTable({ columns, data: rules }) + + const emptyState = ( + + setCreateModalOpen(true)} + /> + + ) + + return ( + <> +
+ + {createModalOpen && ( + setCreateModalOpen(false)} + /> + )} + {editing && ( + setEditing(null)} + /> + )} +
+ {rules.length > 0 ?
: emptyState} + + ) +} diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx new file mode 100644 index 0000000000..262139f282 --- /dev/null +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx @@ -0,0 +1,7 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcSubnetsTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcSubnetsTab.tsx new file mode 100644 index 0000000000..fb2dc5e89d --- /dev/null +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcSubnetsTab.tsx @@ -0,0 +1,78 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { useState } from 'react' + +import { useApiMutation, useApiQueryClient, type VpcSubnet } from '@oxide/api' +import { DateCell, TwoLineCell, useQueryTable, type MenuAction } from '@oxide/table' +import { Button, EmptyMessage } from '@oxide/ui' + +import { CreateSubnetForm } from 'app/forms/subnet-create' +import { EditSubnetForm } from 'app/forms/subnet-edit' +import { useVpcSelector } from 'app/hooks' +import { confirmDelete } from 'app/stores/confirm-delete' + +export const VpcSubnetsTab = () => { + const vpcSelector = useVpcSelector() + const queryClient = useApiQueryClient() + + const { Table, Column } = useQueryTable('vpcSubnetList', { query: vpcSelector }) + const [creating, setCreating] = useState(false) + const [editing, setEditing] = useState(null) + + const deleteSubnet = useApiMutation('vpcSubnetDelete', { + onSuccess() { + queryClient.invalidateQueries('vpcSubnetList') + }, + }) + + const makeActions = (subnet: VpcSubnet): MenuAction[] => [ + { + label: 'Edit', + onActivate: () => setEditing(subnet), + }, + // TODO: only show if you have permission to do this + { + label: 'Delete', + onActivate: confirmDelete({ + doDelete: () => deleteSubnet.mutateAsync({ path: { subnet: subnet.id } }), + label: subnet.name, + }), + }, + ] + + const emptyState = ( + setCreating(true)} + /> + ) + + return ( + <> +
+ + {creating && setCreating(false)} />} + {editing && setEditing(null)} />} +
+
+ + [vpc.ipv4Block, vpc.ipv6Block]} + cell={TwoLineCell} + /> + +
+ + ) +} diff --git a/app/pages/project/vpcs/VpcsPage.tsx b/app/pages/project/vpcs/VpcsPage.tsx new file mode 100644 index 0000000000..286faa08f3 --- /dev/null +++ b/app/pages/project/vpcs/VpcsPage.tsx @@ -0,0 +1,117 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { useMemo } from 'react' +import { Link, Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' + +import { + apiQueryClient, + useApiMutation, + useApiQueryClient, + usePrefetchedApiQuery, + type Vpc, +} from '@oxide/api' +import { DateCell, linkCell, useQueryTable, type MenuAction } from '@oxide/table' +import { + buttonStyle, + EmptyMessage, + Networking24Icon, + PageHeader, + PageTitle, + TableActions, +} from '@oxide/ui' + +import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' +import { confirmDelete } from 'app/stores/confirm-delete' +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={pb.vpcNew(useProjectSelector())} + /> +) + +// just as in the vpcList call for the quick actions menu, include limit: 25 to make +// sure it matches the call in the QueryTable +VpcsPage.loader = async ({ params }: LoaderFunctionArgs) => { + await apiQueryClient.prefetchQuery('vpcList', { + query: { ...getProjectSelector(params), limit: 25 }, + }) + return null +} + +export function VpcsPage() { + const queryClient = useApiQueryClient() + const projectSelector = useProjectSelector() + const { data: vpcs } = usePrefetchedApiQuery('vpcList', { + query: { ...projectSelector, limit: 25 }, // to have same params as QueryTable + }) + const navigate = useNavigate() + + const deleteVpc = useApiMutation('vpcDelete', { + onSuccess() { + queryClient.invalidateQueries('vpcList') + }, + }) + + const makeActions = (vpc: Vpc): MenuAction[] => [ + { + label: 'Edit', + onActivate() { + navigate(pb.vpcEdit({ ...projectSelector, vpc: vpc.name }), { state: vpc }) + }, + }, + { + label: 'Delete', + onActivate: confirmDelete({ + doDelete: () => + deleteVpc.mutateAsync({ path: { vpc: vpc.name }, query: projectSelector }), + label: vpc.name, + }), + }, + ] + + useQuickActions( + useMemo( + () => + vpcs.items.map((v) => ({ + value: v.name, + onSelect: () => navigate(pb.vpc({ ...projectSelector, vpc: v.name })), + navGroup: 'Go to VPC', + })), + [projectSelector, vpcs, navigate] + ) + ) + + const { Table, Column } = useQueryTable('vpcList', { query: projectSelector }) + return ( + <> + + }>VPCs + + + + New Vpc + + + } makeActions={makeActions}> + pb.vpc({ ...projectSelector, vpc }))} + /> + + + +
+ + + ) +} diff --git a/app/pages/project/vpcs/index.tsx b/app/pages/project/vpcs/index.tsx new file mode 100644 index 0000000000..63acdb001a --- /dev/null +++ b/app/pages/project/vpcs/index.tsx @@ -0,0 +1,10 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +export * from './VpcPage' +export * from './VpcsPage' diff --git a/app/routes.tsx b/app/routes.tsx index f7ee339456..2b1c5d67e3 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -47,21 +47,20 @@ import { LoginPageSaml } from './pages/LoginPageSaml' import { instanceLookupLoader } from './pages/lookups' import { DisksPage, + FloatingIpsPage, ImagesPage, InstancePage, InstancesPage, ProjectAccessPage, SnapshotsPage, VpcPage, - VpcsTab, + VpcsPage, } from './pages/project' import { SerialConsolePage } from './pages/project/instances/instance/SerialConsolePage' import { ConnectTab } from './pages/project/instances/instance/tabs/ConnectTab' import { MetricsTab } from './pages/project/instances/instance/tabs/MetricsTab' import { NetworkingTab } from './pages/project/instances/instance/tabs/NetworkingTab' import { StorageTab } from './pages/project/instances/instance/tabs/StorageTab' -import { FloatingIpsTab } from './pages/project/networking/FloatingIpsTab' -import { ProjectNetworkingPage } from './pages/project/networking/ProjectNetworkingPage' import ProjectsPage from './pages/ProjectsPage' import { ProfilePage } from './pages/settings/ProfilePage' import { SSHKeysPage } from './pages/settings/SSHKeysPage' @@ -329,45 +328,42 @@ export const routes = createRoutesFromElements(
- }> - } /> - }> - - } - handle={{ crumb: 'New VPC' }} - /> - } - loader={EditVpcSideModalForm.loader} - handle={{ crumb: 'Edit VPC' }} - /> - - - }> - - } - handle={{ crumb: 'New Floating IP' }} - /> - + }> + + } + handle={{ crumb: 'New VPC' }} + /> + } + loader={EditVpcSideModalForm.loader} + handle={{ crumb: 'Edit VPC' }} + /> - {/* Individual VPC pages don't include top-level VPC navigation */} - - - } - loader={VpcPage.loader} - handle={{ crumb: vpcCrumb }} - /> - + + } + loader={VpcPage.loader} + handle={{ crumb: vpcCrumb }} + /> + } + /> + } + handle={{ crumb: 'New Floating IP' }} + /> + } loader={DisksPage.loader}> `${pb.project(params)}/networking`, - vpcNew: (params: Project) => `${pb.projectNetworking(params)}/vpcs-new`, - vpcs: (params: Project) => `${pb.projectNetworking(params)}/vpcs`, + 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`, - floatingIps: (params: Project) => `${pb.projectNetworking(params)}/floating-ips`, - floatingIpNew: (params: Project) => `${pb.projectNetworking(params)}/floating-ips-new`, + floatingIps: (params: Project) => `${pb.project(params)}/floating-ips`, + floatingIpNew: (params: Project) => `${pb.project(params)}/floating-ips-new`, siloUtilization: () => '/utilization', siloAccess: () => '/access', From 87a3a0dff81ca629a8668e26428471c28f4c48c8 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 15 Feb 2024 15:28:06 -0800 Subject: [PATCH 33/64] Clean up unneeded files --- app/pages/project/vpcs/FloatingIpsTab.tsx | 246 ---------------------- app/routes.tsx | 3 +- 2 files changed, 2 insertions(+), 247 deletions(-) delete mode 100644 app/pages/project/vpcs/FloatingIpsTab.tsx diff --git a/app/pages/project/vpcs/FloatingIpsTab.tsx b/app/pages/project/vpcs/FloatingIpsTab.tsx deleted file mode 100644 index deb80a6145..0000000000 --- a/app/pages/project/vpcs/FloatingIpsTab.tsx +++ /dev/null @@ -1,246 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom' - -import { - apiQueryClient, - useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, - type FloatingIp, - type Instance, -} from '@oxide/api' -import { useQueryTable, type MenuAction } from '@oxide/table' -import { buttonStyle, EmptyMessage, Listbox, Modal, Networking24Icon } from '@oxide/ui' - -import { getProjectSelector, useProjectSelector } from 'app/hooks' -import { confirmDelete } from 'app/stores/confirm-delete' -import { addToast } from 'app/stores/toast' -import { pb } from 'app/util/path-builder' - -const EmptyState = () => ( - } - title="No Floating IPs" - body="You need to create a Floating IP to be able to see it here" - buttonText="New Floating IP" - buttonTo={pb.floatingIpNew(useProjectSelector())} - /> -) - -FloatingIpsTab.loader = async ({ params }: LoaderFunctionArgs) => { - const { project } = getProjectSelector(params) - await Promise.all([ - apiQueryClient.prefetchQuery('floatingIpList', { - query: { project, limit: 25 }, - }), - apiQueryClient.prefetchQuery('instanceList', { - query: { project }, - }), - ]) - return null -} - -export function FloatingIpsTab() { - const [attachModalOpen, setAttachModalOpen] = useState(false) - const [detachModalOpen, setDetachModalOpen] = useState(false) - const [floatingIpToModify, setFloatingIpToModify] = useState(null) - const queryClient = useApiQueryClient() - const { project } = useProjectSelector() - const { data: instances } = usePrefetchedApiQuery('instanceList', { - query: { project }, - }) - - const deleteFloatingIp = useApiMutation('floatingIpDelete', { - onSuccess() { - queryClient.invalidateQueries('floatingIpList') - addToast({ content: 'Your Floating IP has been deleted' }) - }, - }) - - const makeActions = (floatingIp: FloatingIp): MenuAction[] => { - const isAttachedToAnInstance = !!floatingIp.instanceId - return [ - { - label: 'Attach', - disabled: isAttachedToAnInstance - ? 'This floating IP must be detached from the existing instance before it can be attached to a new one' - : false, - onActivate() { - setFloatingIpToModify(floatingIp) - setAttachModalOpen(true) - }, - }, - { - label: 'Detach', - disabled: isAttachedToAnInstance - ? false - : 'This floating IP is not attached to an instance', - onActivate() { - setFloatingIpToModify(floatingIp) - setDetachModalOpen(true) - }, - }, - { - label: 'Delete', - disabled: isAttachedToAnInstance - ? 'This floating IP must be detached from the instance before it can be deleted' - : false, - onActivate: confirmDelete({ - doDelete: () => - deleteFloatingIp.mutateAsync({ - path: { floatingIp: floatingIp.name }, - query: { project }, - }), - label: floatingIp.name, - }), - }, - ] - } - - const getInstanceName = (instanceId: string) => - instances.items.find((i) => i.id === instanceId)?.name - - const { Table, Column } = useQueryTable('floatingIpList', { query: { project } }) - return ( - <> -
- - New Floating IPasdasdasdadasdasdasdaddsafsda as dsad sdasdsad asdasdd ad asdasd - asdasd asdasd asd sa - -
- } makeActions={makeActions}> - - - - getInstanceName(instanceId)} - /> -
- - {attachModalOpen && floatingIpToModify && ( - setAttachModalOpen(false)} - /> - )} - {detachModalOpen && floatingIpToModify?.instanceId && ( - setDetachModalOpen(false)} - /> - )} - - ) -} - -const AttachFloatingIpModal = ({ - floatingIp, - instances, - project, - onDismiss, -}: { - floatingIp: string - instances: Array - project: string - onDismiss: () => void -}) => { - const queryClient = useApiQueryClient() - const floatingIpAttach = useApiMutation('floatingIpAttach', { - onSuccess() { - queryClient.invalidateQueries('floatingIpList') - addToast({ content: 'Your Floating IP has been attached' }) - onDismiss() - }, - onError: (err) => { - addToast({ title: 'Error', content: err.message, variant: 'error' }) - }, - }) - const form = useForm({ defaultValues: { instanceId: '' } }) - - return ( - - - -
- ({ value: i.id, label: i.name }))} - label="Select an instance" - onChange={(e) => { - form.setValue('instanceId', e) - }} - selected={form.watch('instanceId')} - /> - -
-
- - floatingIpAttach.mutate({ - path: { floatingIp }, - query: { project }, - body: { kind: 'instance', parent: form.getValues('instanceId') }, - }) - } - onDismiss={onDismiss} - > -
- ) -} - -const DetachFloatingIpModal = ({ - floatingIp, - instance, - project, - onDismiss, -}: { - floatingIp: string - instance: string - project: string - onDismiss: () => void -}) => { - const queryClient = useApiQueryClient() - const floatingIpDetach = useApiMutation('floatingIpDetach', { - onSuccess() { - queryClient.invalidateQueries('floatingIpList') - addToast({ content: 'Your Floating IP has been detached' }) - onDismiss() - }, - onError: (err) => { - addToast({ title: 'Error', content: err.message, variant: 'error' }) - }, - }) - return ( - - - - Detach {floatingIp} from {instance}? - - - - floatingIpDetach.mutate({ path: { floatingIp }, query: { project } }) - } - onDismiss={onDismiss} - > - - ) -} diff --git a/app/routes.tsx b/app/routes.tsx index 2b1c5d67e3..2f5d6e0f86 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -328,6 +328,8 @@ export const routes = createRoutesFromElements(
+ } /> + }> - Date: Thu, 15 Feb 2024 15:32:50 -0800 Subject: [PATCH 34/64] Refactor - Networking page not needed at all --- .../networking/ProjectNetworkingPage.tsx | 27 ------------------- app/util/path-builder.spec.ts | 2 ++ app/util/path-builder.ts | 1 - 3 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 app/pages/project/networking/ProjectNetworkingPage.tsx diff --git a/app/pages/project/networking/ProjectNetworkingPage.tsx b/app/pages/project/networking/ProjectNetworkingPage.tsx deleted file mode 100644 index 5fa9cbec9e..0000000000 --- a/app/pages/project/networking/ProjectNetworkingPage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import { Networking24Icon, PageHeader, PageTitle } from '@oxide/ui' - -import { RouteTabs, Tab } from 'app/components/RouteTabs' -import { useProjectSelector } from 'app/hooks' -import { pb } from 'app/util/path-builder' - -export function ProjectNetworkingPage() { - const projectSelector = useProjectSelector() - return ( - <> - - }>Networking - - - VPCs - Floating IPs - - - ) -} diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 35b955a681..1ac145f925 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -31,6 +31,8 @@ test('path builder', () => { "diskInventory": "/system/inventory/disks", "diskNew": "/projects/p/disks-new", "disks": "/projects/p/disks", + "floatingIpNew": "/projects/p/floating-ip-new", + "floatingIps": "/projects/p/floating-ips", "instance": "/projects/p/instances/i", "instanceConnect": "/projects/p/instances/i/connect", "instanceMetrics": "/projects/p/instances/i/metrics", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 832651e9e7..4357f5df95 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -62,7 +62,6 @@ export const pb = { snapshotImageCreate: (params: Snapshot) => `${pb.project(params)}/snapshots/${params.snapshot}/image-new`, - projectNetworking: (params: Project) => `${pb.project(params)}/networking`, vpcNew: (params: Project) => `${pb.project(params)}/vpcs-new`, vpcs: (params: Project) => `${pb.project(params)}/vpcs`, vpc: (params: Vpc) => `${pb.vpcs(params)}/${params.vpc}`, From 7e1d68379c2e711088139c37f6142dd952ecaef8 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 15 Feb 2024 15:34:55 -0800 Subject: [PATCH 35/64] Clean up redirect reference --- app/routes.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/routes.tsx b/app/routes.tsx index 2f5d6e0f86..2b1c5d67e3 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -328,8 +328,6 @@ export const routes = createRoutesFromElements( - } /> - }> + Date: Thu, 15 Feb 2024 15:41:31 -0800 Subject: [PATCH 36/64] Consistency on route naming --- app/pages/project/floating-ips/FloatingIpsPage.tsx | 4 ++-- app/test/e2e/networking.e2e.ts | 2 +- app/util/path-builder.spec.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx index 9c40afd121..fab2ab88ef 100644 --- a/app/pages/project/floating-ips/FloatingIpsPage.tsx +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -41,7 +41,7 @@ const EmptyState = () => ( title="No Floating IPs" body="You need to create a Floating IP to be able to see it here" buttonText="New Floating IP" - buttonTo={pb.floatingIpNew(useProjectSelector())} + buttonTo={pb.floatingIpsNew(useProjectSelector())} /> ) @@ -125,7 +125,7 @@ export function FloatingIpsPage() { }>Floating IPs - + New Floating IP diff --git a/app/test/e2e/networking.e2e.ts b/app/test/e2e/networking.e2e.ts index a71c7a00cb..999ef18907 100644 --- a/app/test/e2e/networking.e2e.ts +++ b/app/test/e2e/networking.e2e.ts @@ -12,7 +12,7 @@ import { expectNotVisible, expectVisible } from './utils' test('Create and edit VPC', async ({ page }) => { await page.goto('/projects/mock-project') - await page.click('role=link[name*="Networking"]') + await page.click('role=link[name*="VPCs"]') await expectVisible(page, [ 'role=heading[name*="VPCs"]', 'role=cell[name="mock-vpc"] >> nth=0', diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 1ac145f925..0adf142503 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -31,7 +31,7 @@ test('path builder', () => { "diskInventory": "/system/inventory/disks", "diskNew": "/projects/p/disks-new", "disks": "/projects/p/disks", - "floatingIpNew": "/projects/p/floating-ip-new", + "floatingIpsNew": "/projects/p/floating-ips-new", "floatingIps": "/projects/p/floating-ips", "instance": "/projects/p/instances/i", "instanceConnect": "/projects/p/instances/i/connect", From 7a8a31f21ddfcefa68f9e0151a6061404bc88fcf Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 15 Feb 2024 15:45:27 -0800 Subject: [PATCH 37/64] One more consistency fix --- app/util/path-builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 4357f5df95..cdfa9d1d42 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -67,7 +67,7 @@ export const pb = { vpc: (params: Vpc) => `${pb.vpcs(params)}/${params.vpc}`, vpcEdit: (params: Vpc) => `${pb.vpc(params)}/edit`, floatingIps: (params: Project) => `${pb.project(params)}/floating-ips`, - floatingIpNew: (params: Project) => `${pb.project(params)}/floating-ips-new`, + floatingIpsNew: (params: Project) => `${pb.project(params)}/floating-ips-new`, siloUtilization: () => '/utilization', siloAccess: () => '/access', From 052ec9cf8c1ab02d455623e907d04245ce67c524 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 15 Feb 2024 16:22:17 -0800 Subject: [PATCH 38/64] Make the linter happy --- app/forms/floating-ip-create.tsx | 2 +- app/util/path-builder.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index abc103d5b5..e8e2451add 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -69,7 +69,7 @@ export function CreateFloatingIpSideModalForm({ }) const defaultPool = useMemo( - () => (allPools ? allPools.items.find((p) => p.isDefault)?.name : undefined), + () => allPools?.items.find((p) => p.isDefault)?.name, [allPools] ) diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 0adf142503..c475b97827 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -31,8 +31,8 @@ test('path builder', () => { "diskInventory": "/system/inventory/disks", "diskNew": "/projects/p/disks-new", "disks": "/projects/p/disks", - "floatingIpsNew": "/projects/p/floating-ips-new", "floatingIps": "/projects/p/floating-ips", + "floatingIpsNew": "/projects/p/floating-ips-new", "instance": "/projects/p/instances/i", "instanceConnect": "/projects/p/instances/i/connect", "instanceMetrics": "/projects/p/instances/i/metrics", From b7bdc0e75300a81152e4088f4314a06a8ae7869f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 15 Feb 2024 16:28:40 -0800 Subject: [PATCH 39/64] Update routes so Floating IP page doesn't disappear when using create form --- app/routes.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/routes.tsx b/app/routes.tsx index 2b1c5d67e3..fc2b73d087 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -352,17 +352,14 @@ export const routes = createRoutesFromElements( /> - } - /> - } - handle={{ crumb: 'New Floating IP' }} - /> + }> + + } + handle={{ crumb: 'New Floating IP' }} + /> + } loader={DisksPage.loader}> Date: Thu, 15 Feb 2024 16:40:11 -0800 Subject: [PATCH 40/64] Fix e2e test --- app/test/e2e/vpcs.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/test/e2e/vpcs.e2e.ts b/app/test/e2e/vpcs.e2e.ts index e0b5f2e0ad..b9a35c053b 100644 --- a/app/test/e2e/vpcs.e2e.ts +++ b/app/test/e2e/vpcs.e2e.ts @@ -10,7 +10,7 @@ import { expect, test } from '@playwright/test' test('can nav to VpcPage from /', async ({ page }) => { await page.goto('/') await page.click('table :text("mock-project")') - await page.click('a:has-text("Networking")') + await page.click('a:has-text("VPCs")') await page.click('a:has-text("mock-vpc")') await expect(page.locator('text=mock-subnet')).toBeVisible() expect(await page.title()).toEqual('mock-vpc / VPCs / mock-project / Oxide Console') From b744d63e439be647c4e571a88cb717864092903f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 08:25:26 -0800 Subject: [PATCH 41/64] Update libs/api-mocks/msw/handlers.ts Co-authored-by: David Crespo --- libs/api-mocks/msw/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 5cf79905ff..70b4d2cf0f 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -230,7 +230,7 @@ export const handlers = makeHandlers({ const newFloatingIp: Json = { id: uuid(), project_id: project.id, - ip: `${[...Array(4)].map(() => Math.floor(Math.random() * 256)).join('.')}`, + ip: [...Array(4)].map(() => Math.floor(Math.random() * 256)).join('.'), ...body, ...getTimestamps(), } From 433d6a1b182afd8464c55964668a516afc4c3318 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 09:17:23 -0800 Subject: [PATCH 42/64] Add InstanceLinkCell component --- app/pages/project/disks/DisksPage.tsx | 24 ++------------ .../project/floating-ips/FloatingIpsPage.tsx | 4 +-- libs/table/cells/InstanceLinkCell.tsx | 33 +++++++++++++++++++ libs/table/cells/index.ts | 1 + 4 files changed, 38 insertions(+), 24 deletions(-) create mode 100644 libs/table/cells/InstanceLinkCell.tsx diff --git a/app/pages/project/disks/DisksPage.tsx b/app/pages/project/disks/DisksPage.tsx index f5ccc0a0b9..5c6fc1ae07 100644 --- a/app/pages/project/disks/DisksPage.tsx +++ b/app/pages/project/disks/DisksPage.tsx @@ -12,15 +12,13 @@ import { diskCan, genName, useApiMutation, - useApiQuery, useApiQueryClient, type Disk, } from '@oxide/api' import { DateCell, - LinkCell, + InstanceLinkCell, SizeCell, - SkeletonCell, useQueryTable, type MenuAction, } from '@oxide/table' @@ -40,24 +38,6 @@ import { pb } from 'app/util/path-builder' import { fancifyStates } from '../instances/instance/tabs/common' -function InstanceNameFromId({ value: instanceId }: { value: string | null }) { - const { project } = useProjectSelector() - const { data: instance } = useApiQuery( - 'instanceView', - { path: { instance: instanceId! } }, - { enabled: !!instanceId } - ) - - if (!instanceId) return null - if (!instance) return - - return ( - - {instance.name} - - ) -} - const EmptyState = () => ( } @@ -157,7 +137,7 @@ export function DisksPage() { // whether it has an instance field 'instance' in disk.state ? disk.state.instance : null } - cell={InstanceNameFromId} + cell={InstanceLinkCell} /> getInstanceName(instanceId)} + cell={InstanceLinkCell} /> diff --git a/libs/table/cells/InstanceLinkCell.tsx b/libs/table/cells/InstanceLinkCell.tsx new file mode 100644 index 0000000000..a525b3d96f --- /dev/null +++ b/libs/table/cells/InstanceLinkCell.tsx @@ -0,0 +1,33 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { useApiQuery } from '@oxide/api' + +import { useProjectSelector } from 'app/hooks' +import { pb } from 'app/util/path-builder' + +import { SkeletonCell } from './EmptyCell' +import { LinkCell } from './LinkCell' + +export const InstanceLinkCell = ({ value: instanceId }: { value: string | null }) => { + const { project } = useProjectSelector() + const { data: instance } = useApiQuery( + 'instanceView', + { path: { instance: instanceId! } }, + { enabled: !!instanceId } + ) + + if (!instanceId) return null + if (!instance) return + + return ( + + {instance.name} + + ) +} diff --git a/libs/table/cells/index.ts b/libs/table/cells/index.ts index f6d81a5d57..1034543f0a 100644 --- a/libs/table/cells/index.ts +++ b/libs/table/cells/index.ts @@ -13,6 +13,7 @@ export * from './DefaultCell' export * from './EnabledCell' export * from './EmptyCell' export * from './FirewallFilterCell' +export * from './InstanceLinkCell' export * from './InstanceResourceCell' export * from './InstanceStatusCell' export * from './LabelCell' From af2741dfe5663c840d8ccedba5385c1c6018e5da Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 11:44:25 -0800 Subject: [PATCH 43/64] Move detach flow to action menu --- .../project/floating-ips/FloatingIpsPage.tsx | 93 ++++++++----------- 1 file changed, 37 insertions(+), 56 deletions(-) diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx index d99bb7ec16..fa15d9df06 100644 --- a/app/pages/project/floating-ips/FloatingIpsPage.tsx +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -30,7 +30,9 @@ import { TableActions, } from '@oxide/ui' +import { HL } from 'app/components/HL' import { getProjectSelector, useProjectSelector } from 'app/hooks' +import { confirmAction } from 'app/stores/confirm-action' import { confirmDelete } from 'app/stores/confirm-delete' import { addToast } from 'app/stores/toast' import { pb } from 'app/util/path-builder' @@ -60,14 +62,24 @@ FloatingIpsPage.loader = async ({ params }: LoaderFunctionArgs) => { export function FloatingIpsPage() { const [attachModalOpen, setAttachModalOpen] = useState(false) - const [detachModalOpen, setDetachModalOpen] = useState(false) const [floatingIpToModify, setFloatingIpToModify] = useState(null) const queryClient = useApiQueryClient() const { project } = useProjectSelector() const { data: instances } = usePrefetchedApiQuery('instanceList', { query: { project }, }) + const getInstanceName = (instanceId: string) => + instances.items.find((i) => i.id === instanceId)?.name + const floatingIpDetach = useApiMutation('floatingIpDetach', { + onSuccess() { + queryClient.invalidateQueries('floatingIpList') + addToast({ content: 'Your Floating IP has been detached' }) + }, + onError: (err) => { + addToast({ title: 'Error', content: err.message, variant: 'error' }) + }, + }) const deleteFloatingIp = useApiMutation('floatingIpDelete', { onSuccess() { queryClient.invalidateQueries('floatingIpList') @@ -93,10 +105,30 @@ export function FloatingIpsPage() { disabled: isAttachedToAnInstance ? false : 'This floating IP is not attached to an instance', - onActivate() { - setFloatingIpToModify(floatingIp) - setDetachModalOpen(true) - }, + onActivate: () => + confirmAction({ + actionType: 'danger', + doAction: () => + floatingIpDetach.mutateAsync({ + path: { floatingIp: floatingIp.name }, + query: { project }, + }), + modalTitle: 'Detach Floating IP', + modalContent: ( +

+ Are you sure you want to detach the floating IP {floatingIp.name}{' '} + from the instance{' '} + + { + // instanceId is guaranteed to be non-null here + getInstanceName(floatingIp.instanceId!) + } + + ? +

+ ), + errorTitle: 'Error detaching floating IP', + }), }, { label: 'Delete', @@ -115,9 +147,6 @@ export function FloatingIpsPage() { ] } - const getInstanceName = (instanceId: string) => - instances.items.find((i) => i.id === instanceId)?.name - const { Table, Column } = useQueryTable('floatingIpList', { query: { project } }) return ( <> @@ -148,14 +177,6 @@ export function FloatingIpsPage() { onDismiss={() => setAttachModalOpen(false)} /> )} - {detachModalOpen && floatingIpToModify?.instanceId && ( - setDetachModalOpen(false)} - /> - )} ) } @@ -216,43 +237,3 @@ const AttachFloatingIpModal = ({ ) } - -const DetachFloatingIpModal = ({ - floatingIp, - instance, - project, - onDismiss, -}: { - floatingIp: string - instance: string - project: string - onDismiss: () => void -}) => { - const queryClient = useApiQueryClient() - const floatingIpDetach = useApiMutation('floatingIpDetach', { - onSuccess() { - queryClient.invalidateQueries('floatingIpList') - addToast({ content: 'Your Floating IP has been detached' }) - onDismiss() - }, - onError: (err) => { - addToast({ title: 'Error', content: err.message, variant: 'error' }) - }, - }) - return ( - - - - Detach {floatingIp} from {instance}? - - - - floatingIpDetach.mutate({ path: { floatingIp }, query: { project } }) - } - onDismiss={onDismiss} - > - - ) -} From 08c654d7b31e015b0627ec94b7e70214c8ceb006 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 11:55:30 -0800 Subject: [PATCH 44/64] Refactor loader to get instance list loading appropriately --- app/forms/floating-ip-create.tsx | 38 ++++++++++++-------------------- app/routes.tsx | 1 + 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index e8e2451add..fa80d4c4ce 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -7,17 +7,13 @@ */ import { useMemo } from 'react' import { useWatch } from 'react-hook-form' -import { - useNavigate, - type LoaderFunctionArgs, - type NavigateFunction, -} from 'react-router-dom' +import { useNavigate, type NavigateFunction } from 'react-router-dom' import { apiQueryClient, useApiMutation, - useApiQuery, useApiQueryClient, + usePrefetchedApiQuery, type FloatingIp, type FloatingIpCreate, type SiloIpPool, @@ -31,12 +27,12 @@ import { SideModalForm, TextField, } from 'app/components/form' -import { getProjectSelector, useForm, useProjectSelector, useToast } from 'app/hooks' +import { useForm, useProjectSelector, useToast } from 'app/hooks' import { pb } from 'app/util/path-builder' -CreateFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { +CreateFloatingIpSideModalForm.loader = async () => { await apiQueryClient.prefetchQuery('projectIpPoolList', { - query: { ...getProjectSelector(params), limit: 1000 }, + query: { limit: 1000 }, }) return null } @@ -60,23 +56,19 @@ export function CreateFloatingIpSideModalForm({ onSubmit, onSuccess, }: CreateSideModalFormProps) { - // Fetch 1000 to we can be sure to get them all. There should only be a few - // anyway. Not prefetched because the prefetched one only gets 25 to match the - // query table. This req is better to do async because they can't click make - // default that fast anyway. - const { data: allPools } = useApiQuery('projectIpPoolList', { + // Fetch 1000 to we can be sure to get them all. + const { data: allPools } = usePrefetchedApiQuery('projectIpPoolList', { query: { limit: 1000 }, }) const defaultPool = useMemo( - () => allPools?.items.find((p) => p.isDefault)?.name, + () => allPools.items.find((p) => p.isDefault)?.name, [allPools] ) const defaultValues: FloatingIpCreate = { name: '', description: '', - // defaultPool doesn't seem to be getting set in the form when page is loaded directly pool: defaultPool, address: undefined, } @@ -137,14 +129,12 @@ export function CreateFloatingIpSideModalForm({ > - {allPools && ( - toListboxItem(p))} - label="Pool" - control={form.control} - /> - )} + toListboxItem(p))} + label="Pool" + control={form.control} + /> } handle={{ crumb: 'New Floating IP' }} /> From 5756309f8494dcaf3ac29ed4af7a4177ce77f228 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 11:58:22 -0800 Subject: [PATCH 45/64] Remove unneeded file --- .../project/networking/VpcPage/VpcPage.tsx | 75 --------- .../project/networking/VpcPage/index.tsx | 9 - .../VpcPage/tabs/VpcFirewallRulesTab.tsx | 155 ------------------ .../VpcPage/tabs/VpcGatewaysTab.tsx | 7 - .../networking/VpcPage/tabs/VpcSubnetsTab.tsx | 78 --------- app/pages/project/networking/VpcsPage.tsx | 117 ------------- app/pages/project/networking/index.tsx | 10 -- app/pages/project/vpcs/VpcPage/VpcPage.tsx | 4 +- 8 files changed, 1 insertion(+), 454 deletions(-) delete mode 100644 app/pages/project/networking/VpcPage/VpcPage.tsx delete mode 100644 app/pages/project/networking/VpcPage/index.tsx delete mode 100644 app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx delete mode 100644 app/pages/project/networking/VpcPage/tabs/VpcGatewaysTab.tsx delete mode 100644 app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx delete mode 100644 app/pages/project/networking/VpcsPage.tsx delete mode 100644 app/pages/project/networking/index.tsx diff --git a/app/pages/project/networking/VpcPage/VpcPage.tsx b/app/pages/project/networking/VpcPage/VpcPage.tsx deleted file mode 100644 index a6286429bc..0000000000 --- a/app/pages/project/networking/VpcPage/VpcPage.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import type { LoaderFunctionArgs } from 'react-router-dom' - -import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' -import { Networking24Icon, PageHeader, PageTitle, PropertiesTable, Tabs } from '@oxide/ui' -import { formatDateTime } from '@oxide/util' - -import { QueryParamTabs } from 'app/components/QueryParamTabs' -import { getVpcSelector, useVpcSelector } from 'app/hooks' - -import { VpcFirewallRulesTab } from './tabs/VpcFirewallRulesTab' -import { VpcSubnetsTab } from './tabs/VpcSubnetsTab' - -VpcPage.loader = async ({ params }: LoaderFunctionArgs) => { - const { project, vpc } = getVpcSelector(params) - await Promise.all([ - apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } }), - apiQueryClient.prefetchQuery('vpcFirewallRulesView', { - query: { project, vpc }, - }), - apiQueryClient.prefetchQuery('vpcSubnetList', { - query: { project, vpc, limit: 25 }, - }), - ]) - return null -} - -export function VpcPage() { - const { project, vpc: vpcName } = useVpcSelector() - const { data: vpc } = usePrefetchedApiQuery('vpcView', { - path: { vpc: vpcName }, - query: { project }, - }) - - return ( - <> - - }>{vpc.name} - - - - {vpc.description} - {vpc.dnsName} - - - - {formatDateTime(vpc.timeCreated)} - - - {formatDateTime(vpc.timeModified)} - - - - - - - Subnets - Firewall Rules - - - - - - - - - - ) -} diff --git a/app/pages/project/networking/VpcPage/index.tsx b/app/pages/project/networking/VpcPage/index.tsx deleted file mode 100644 index c49ee5e598..0000000000 --- a/app/pages/project/networking/VpcPage/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ - -export * from './VpcPage' diff --git a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx deleted file mode 100644 index c955905662..0000000000 --- a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import { useMemo, useState } from 'react' - -import { - useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, - type VpcFirewallRule, -} from '@oxide/api' -import { - ButtonCell, - createColumnHelper, - DateCell, - EnabledCell, - FirewallFilterCell, - getActionsCol, - Table, - TypeValueListCell, - useReactTable, -} from '@oxide/table' -import { Button, EmptyMessage, TableEmptyBox } from '@oxide/ui' -import { sortBy, titleCase } from '@oxide/util' - -import { CreateFirewallRuleForm } from 'app/forms/firewall-rules-create' -import { EditFirewallRuleForm } from 'app/forms/firewall-rules-edit' -import { useVpcSelector } from 'app/hooks' -import { confirmDelete } from 'app/stores/confirm-delete' - -const colHelper = createColumnHelper() - -/** columns that don't depend on anything in `render` */ -const staticColumns = [ - colHelper.accessor('priority', { - header: 'Priority', - cell: (info) =>
{info.getValue()}
, - }), - colHelper.accessor('action', { - header: 'Action', - cell: (info) =>
{titleCase(info.getValue())}
, - }), - colHelper.accessor('direction', { - header: 'Direction', - cell: (info) =>
{titleCase(info.getValue())}
, - }), - colHelper.accessor('targets', { - header: 'Targets', - cell: (info) => , - }), - colHelper.accessor('filters', { - header: 'Filters', - cell: (info) => , - }), - colHelper.accessor('status', { - header: 'Status', - cell: (info) => , - }), - colHelper.accessor('timeCreated', { - id: 'created', - header: 'Created', - cell: (info) => , - }), -] - -export const VpcFirewallRulesTab = () => { - const queryClient = useApiQueryClient() - const vpcSelector = useVpcSelector() - - const { data } = usePrefetchedApiQuery('vpcFirewallRulesView', { - query: vpcSelector, - }) - const rules = useMemo(() => sortBy(data.rules, (r) => r.priority), [data]) - - const [createModalOpen, setCreateModalOpen] = useState(false) - const [editing, setEditing] = useState(null) - - const updateRules = useApiMutation('vpcFirewallRulesUpdate', { - onSuccess() { - queryClient.invalidateQueries('vpcFirewallRulesView') - }, - }) - - // the whole thing can't be static because the action depends on setEditing - const columns = useMemo(() => { - return [ - colHelper.accessor('name', { - header: 'Name', - cell: (info) => ( - setEditing(info.row.original)}> - {info.getValue()} - - ), - }), - ...staticColumns, - getActionsCol((rule: VpcFirewallRule) => [ - { label: 'Edit', onActivate: () => setEditing(rule) }, - { - label: 'Delete', - onActivate: confirmDelete({ - doDelete: () => - updateRules.mutateAsync({ - query: vpcSelector, - body: { - rules: rules.filter((r) => r.id !== rule.id), - }, - }), - label: rule.name, - }), - }, - ]), - ] - }, [setEditing, rules, updateRules, vpcSelector]) - - const table = useReactTable({ columns, data: rules }) - - const emptyState = ( - - setCreateModalOpen(true)} - /> - - ) - - return ( - <> -
- - {createModalOpen && ( - setCreateModalOpen(false)} - /> - )} - {editing && ( - setEditing(null)} - /> - )} -
- {rules.length > 0 ? : emptyState} - - ) -} diff --git a/app/pages/project/networking/VpcPage/tabs/VpcGatewaysTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcGatewaysTab.tsx deleted file mode 100644 index 262139f282..0000000000 --- a/app/pages/project/networking/VpcPage/tabs/VpcGatewaysTab.tsx +++ /dev/null @@ -1,7 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ diff --git a/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx deleted file mode 100644 index fb2dc5e89d..0000000000 --- a/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import { useState } from 'react' - -import { useApiMutation, useApiQueryClient, type VpcSubnet } from '@oxide/api' -import { DateCell, TwoLineCell, useQueryTable, type MenuAction } from '@oxide/table' -import { Button, EmptyMessage } from '@oxide/ui' - -import { CreateSubnetForm } from 'app/forms/subnet-create' -import { EditSubnetForm } from 'app/forms/subnet-edit' -import { useVpcSelector } from 'app/hooks' -import { confirmDelete } from 'app/stores/confirm-delete' - -export const VpcSubnetsTab = () => { - const vpcSelector = useVpcSelector() - const queryClient = useApiQueryClient() - - const { Table, Column } = useQueryTable('vpcSubnetList', { query: vpcSelector }) - const [creating, setCreating] = useState(false) - const [editing, setEditing] = useState(null) - - const deleteSubnet = useApiMutation('vpcSubnetDelete', { - onSuccess() { - queryClient.invalidateQueries('vpcSubnetList') - }, - }) - - const makeActions = (subnet: VpcSubnet): MenuAction[] => [ - { - label: 'Edit', - onActivate: () => setEditing(subnet), - }, - // TODO: only show if you have permission to do this - { - label: 'Delete', - onActivate: confirmDelete({ - doDelete: () => deleteSubnet.mutateAsync({ path: { subnet: subnet.id } }), - label: subnet.name, - }), - }, - ] - - const emptyState = ( - setCreating(true)} - /> - ) - - return ( - <> -
- - {creating && setCreating(false)} />} - {editing && setEditing(null)} />} -
-
- - [vpc.ipv4Block, vpc.ipv6Block]} - cell={TwoLineCell} - /> - -
- - ) -} diff --git a/app/pages/project/networking/VpcsPage.tsx b/app/pages/project/networking/VpcsPage.tsx deleted file mode 100644 index 286faa08f3..0000000000 --- a/app/pages/project/networking/VpcsPage.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import { useMemo } from 'react' -import { Link, Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' - -import { - apiQueryClient, - useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, - type Vpc, -} from '@oxide/api' -import { DateCell, linkCell, useQueryTable, type MenuAction } from '@oxide/table' -import { - buttonStyle, - EmptyMessage, - Networking24Icon, - PageHeader, - PageTitle, - TableActions, -} from '@oxide/ui' - -import { getProjectSelector, useProjectSelector, useQuickActions } from 'app/hooks' -import { confirmDelete } from 'app/stores/confirm-delete' -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={pb.vpcNew(useProjectSelector())} - /> -) - -// just as in the vpcList call for the quick actions menu, include limit: 25 to make -// sure it matches the call in the QueryTable -VpcsPage.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('vpcList', { - query: { ...getProjectSelector(params), limit: 25 }, - }) - return null -} - -export function VpcsPage() { - const queryClient = useApiQueryClient() - const projectSelector = useProjectSelector() - const { data: vpcs } = usePrefetchedApiQuery('vpcList', { - query: { ...projectSelector, limit: 25 }, // to have same params as QueryTable - }) - const navigate = useNavigate() - - const deleteVpc = useApiMutation('vpcDelete', { - onSuccess() { - queryClient.invalidateQueries('vpcList') - }, - }) - - const makeActions = (vpc: Vpc): MenuAction[] => [ - { - label: 'Edit', - onActivate() { - navigate(pb.vpcEdit({ ...projectSelector, vpc: vpc.name }), { state: vpc }) - }, - }, - { - label: 'Delete', - onActivate: confirmDelete({ - doDelete: () => - deleteVpc.mutateAsync({ path: { vpc: vpc.name }, query: projectSelector }), - label: vpc.name, - }), - }, - ] - - useQuickActions( - useMemo( - () => - vpcs.items.map((v) => ({ - value: v.name, - onSelect: () => navigate(pb.vpc({ ...projectSelector, vpc: v.name })), - navGroup: 'Go to VPC', - })), - [projectSelector, vpcs, navigate] - ) - ) - - const { Table, Column } = useQueryTable('vpcList', { query: projectSelector }) - return ( - <> - - }>VPCs - - - - New Vpc - - - } makeActions={makeActions}> - pb.vpc({ ...projectSelector, vpc }))} - /> - - - -
- - - ) -} diff --git a/app/pages/project/networking/index.tsx b/app/pages/project/networking/index.tsx deleted file mode 100644 index 63acdb001a..0000000000 --- a/app/pages/project/networking/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ - -export * from './VpcPage' -export * from './VpcsPage' diff --git a/app/pages/project/vpcs/VpcPage/VpcPage.tsx b/app/pages/project/vpcs/VpcPage/VpcPage.tsx index 6ab708ed5b..a6286429bc 100644 --- a/app/pages/project/vpcs/VpcPage/VpcPage.tsx +++ b/app/pages/project/vpcs/VpcPage/VpcPage.tsx @@ -41,9 +41,7 @@ export function VpcPage() { return ( <> - }> - {vpc.name} adasdasdasdasdasdasdasdadasdasdasd - + }>{vpc.name} From 0df20f1d53a82c9887198753cbf4c7059614b81c Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 12:01:54 -0800 Subject: [PATCH 46/64] Refactor props for floating ip create form --- app/forms/floating-ip-create.tsx | 33 +++++--------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index fa80d4c4ce..0838bb99c8 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -7,14 +7,13 @@ */ import { useMemo } from 'react' import { useWatch } from 'react-hook-form' -import { useNavigate, type NavigateFunction } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { apiQueryClient, useApiMutation, useApiQueryClient, usePrefetchedApiQuery, - type FloatingIp, type FloatingIpCreate, type SiloIpPool, } from '@oxide/api' @@ -37,25 +36,7 @@ CreateFloatingIpSideModalForm.loader = async () => { return null } -type CreateSideModalFormProps = { - /** - * If defined, this overrides the usual mutation. Caller is responsible for - * doing a dismiss behavior in onSubmit as well, because we are not calling - * the RQ `onSuccess` defined for the mutation. - */ - onSubmit?: (floatingIpCreate: FloatingIpCreate) => void - /** - * Passing navigate is a bit of a hack to be able to do a nav from the routes - * file. The callers that don't need the arg can ignore it. - */ - onDismiss?: (navigate: NavigateFunction) => void - onSuccess?: (floatingIp: FloatingIp) => void -} - -export function CreateFloatingIpSideModalForm({ - onSubmit, - onSuccess, -}: CreateSideModalFormProps) { +export function CreateFloatingIpSideModalForm() { // Fetch 1000 to we can be sure to get them all. const { data: allPools } = usePrefetchedApiQuery('projectIpPoolList', { query: { limit: 1000 }, @@ -79,10 +60,9 @@ export function CreateFloatingIpSideModalForm({ const navigate = useNavigate() const createFloatingIp = useApiMutation('floatingIpCreate', { - onSuccess(data) { + onSuccess() { queryClient.invalidateQueries('floatingIpList') addToast({ content: 'Your Floating IP has been created' }) - onSuccess?.(data) navigate(pb.floatingIps(projectSelector)) }, }) @@ -118,11 +98,8 @@ export function CreateFloatingIpSideModalForm({ title="Create Floating IP" form={form} onDismiss={() => navigate(pb.floatingIps(projectSelector))} - onSubmit={({ ...rest }) => { - const body = { ...rest } - onSubmit - ? onSubmit(body) - : createFloatingIp.mutate({ query: projectSelector, body }) + onSubmit={(body) => { + createFloatingIp.mutate({ query: projectSelector, body }) }} loading={createFloatingIp.isPending} submitError={createFloatingIp.error} From c7af4dc8784f989fd9c397813cd14d8fcf45b04a Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 12:04:13 -0800 Subject: [PATCH 47/64] Align path for floatingIpNew with standard syntax --- app/pages/project/floating-ips/FloatingIpsPage.tsx | 4 ++-- app/util/path-builder.spec.ts | 2 +- app/util/path-builder.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx index fa15d9df06..b95dc794b2 100644 --- a/app/pages/project/floating-ips/FloatingIpsPage.tsx +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -43,7 +43,7 @@ const EmptyState = () => ( title="No Floating IPs" body="You need to create a Floating IP to be able to see it here" buttonText="New Floating IP" - buttonTo={pb.floatingIpsNew(useProjectSelector())} + buttonTo={pb.floatingIpNew(useProjectSelector())} /> ) @@ -154,7 +154,7 @@ export function FloatingIpsPage() { }>Floating IPs - + New Floating IP diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index c475b97827..6476b096a3 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -31,8 +31,8 @@ test('path builder', () => { "diskInventory": "/system/inventory/disks", "diskNew": "/projects/p/disks-new", "disks": "/projects/p/disks", + "floatingIpNew": "/projects/p/floating-ips-new", "floatingIps": "/projects/p/floating-ips", - "floatingIpsNew": "/projects/p/floating-ips-new", "instance": "/projects/p/instances/i", "instanceConnect": "/projects/p/instances/i/connect", "instanceMetrics": "/projects/p/instances/i/metrics", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index cdfa9d1d42..4357f5df95 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -67,7 +67,7 @@ export const pb = { vpc: (params: Vpc) => `${pb.vpcs(params)}/${params.vpc}`, vpcEdit: (params: Vpc) => `${pb.vpc(params)}/edit`, floatingIps: (params: Project) => `${pb.project(params)}/floating-ips`, - floatingIpsNew: (params: Project) => `${pb.project(params)}/floating-ips-new`, + floatingIpNew: (params: Project) => `${pb.project(params)}/floating-ips-new`, siloUtilization: () => '/utilization', siloAccess: () => '/access', From b700959179ea190b15bceca46b2853b8939792f3 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 12:09:47 -0800 Subject: [PATCH 48/64] Use floatingIp lookup Co-authored-by: David Crespo --- libs/api-mocks/msw/handlers.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 70b4d2cf0f..652656ba12 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -242,13 +242,7 @@ export const handlers = makeHandlers({ const ips = db.floatingIps.filter((i) => i.project_id === project.id) return paginated(query, ips) }, - floatingIpView({ query, path }) { - const project = lookup.project(query) - const ip = db.floatingIps.filter( - (i) => i.project_id === project.id && i.name === path.floatingIp - )[0] - return ip - }, + floatingIp: ({ path, query }) => lookup.floatingIp({ ...path, ...query }), floatingIpDelete({ path, query }) { const floatingIp = lookup.floatingIp({ ...path, ...query }) db.floatingIps = db.floatingIps.filter((i) => i.id !== floatingIp.id) From 10bce524094c4498f390daa287cfc6ced988a1d7 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 12:16:00 -0800 Subject: [PATCH 49/64] Make sure an already-attached floating IP can't be attached again --- libs/api-mocks/msw/handlers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 652656ba12..6044df9d0d 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -242,7 +242,7 @@ export const handlers = makeHandlers({ const ips = db.floatingIps.filter((i) => i.project_id === project.id) return paginated(query, ips) }, - floatingIp: ({ path, query }) => lookup.floatingIp({ ...path, ...query }), + floatingIpView: ({ path, query }) => lookup.floatingIp({ ...path, ...query }), floatingIpDelete({ path, query }) { const floatingIp = lookup.floatingIp({ ...path, ...query }) db.floatingIps = db.floatingIps.filter((i) => i.id !== floatingIp.id) @@ -251,6 +251,9 @@ export const handlers = makeHandlers({ }, floatingIpAttach({ path, query, body }) { const floatingIp = lookup.floatingIp({ ...path, ...query }) + if (floatingIp.instance_id) { + throw 'floating IP cannot be attached to one instance while still attached to another' + } const instance = lookup.instance({ ...path, ...query, instance: body.parent }) floatingIp.instance_id = instance.id From 77246eb0e92969503a957a53bc1249394d69bc6e Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 12:38:52 -0800 Subject: [PATCH 50/64] Update type on Floating IP Create - address value, and prevent spaces in field --- app/forms/floating-ip-create.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index 0838bb99c8..bb4410d789 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -8,6 +8,7 @@ import { useMemo } from 'react' import { useWatch } from 'react-hook-form' import { useNavigate } from 'react-router-dom' +import type { SetRequired } from 'type-fest' import { apiQueryClient, @@ -47,11 +48,11 @@ export function CreateFloatingIpSideModalForm() { [allPools] ) - const defaultValues: FloatingIpCreate = { + const defaultValues: SetRequired = { name: '', description: '', pool: defaultPool, - address: undefined, + address: '', } const queryClient = useApiQueryClient() @@ -98,8 +99,12 @@ export function CreateFloatingIpSideModalForm() { title="Create Floating IP" form={form} onDismiss={() => navigate(pb.floatingIps(projectSelector))} - onSubmit={(body) => { - createFloatingIp.mutate({ query: projectSelector, body }) + onSubmit={({ address, ...rest }) => { + createFloatingIp.mutate({ + query: projectSelector, + // if address is '', evaluate as false and send as undefined + body: { address: address || undefined, ...rest }, + }) }} loading={createFloatingIp.isPending} submitError={createFloatingIp.error} @@ -116,7 +121,7 @@ export function CreateFloatingIpSideModalForm() { name="address" control={form.control} disabled={!isPoolSelected} - transform={(ip) => (ip.trim() === '' ? undefined : ip)} + transform={(v) => v.replace(/\s/g, '')} validate={(ip) => (ip && !validateIp(ip).valid ? 'Not a valid IP address' : true)} /> From c9134afe1256904706d5da668f84dbea5aea7eab Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 13:06:16 -0800 Subject: [PATCH 51/64] Refactor forms and types --- app/forms/floating-ip-create.tsx | 25 +++++++++++-------- .../project/floating-ips/FloatingIpsPage.tsx | 3 +++ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index bb4410d789..3032c35084 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -5,7 +5,6 @@ * * Copyright Oxide Computer Company */ -import { useMemo } from 'react' import { useWatch } from 'react-hook-form' import { useNavigate } from 'react-router-dom' import type { SetRequired } from 'type-fest' @@ -18,6 +17,7 @@ import { type FloatingIpCreate, type SiloIpPool, } from '@oxide/api' +import { Badge, Divider, Message } from '@oxide/ui' import { validateIp } from '@oxide/util' import { @@ -43,15 +43,10 @@ export function CreateFloatingIpSideModalForm() { query: { limit: 1000 }, }) - const defaultPool = useMemo( - () => allPools.items.find((p) => p.isDefault)?.name, - [allPools] - ) - const defaultValues: SetRequired = { name: '', description: '', - pool: defaultPool, + pool: undefined, address: '', } @@ -71,7 +66,7 @@ export function CreateFloatingIpSideModalForm() { const form = useForm({ defaultValues }) const toListboxItem = (p: SiloIpPool) => { - if (p.name !== defaultPool) { + if (!p.isDefault) { return { value: p.name, label: p.name, @@ -84,7 +79,9 @@ export function CreateFloatingIpSideModalForm() { label: ( <> {p.name}{' '} - (default) + + default + ), } @@ -109,13 +106,21 @@ export function CreateFloatingIpSideModalForm() { loading={createFloatingIp.isPending} submitError={createFloatingIp.error} > + + + + {/* Todo: collapse these two fields under an "advanced" section */} toListboxItem(p))} - label="Pool" + label="IP pool" control={form.control} + placeholder="Select pool" /> + {/* Todo: Add help text explaining what selecting an instance will do */}
{ form.setValue('instanceId', e) }} + required + placeholder="Select instance" selected={form.watch('instanceId')} /> From c148a989f49da7ea66c74a910f59b19ffa006c18 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 16 Feb 2024 13:14:55 -0800 Subject: [PATCH 52/64] Refactor --- app/forms/floating-ip-create.tsx | 60 ++++++++++++++------------------ 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index 3032c35084..45f24fbb26 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -5,7 +5,6 @@ * * Copyright Oxide Computer Company */ -import { useWatch } from 'react-hook-form' import { useNavigate } from 'react-router-dom' import type { SetRequired } from 'type-fest' @@ -37,19 +36,38 @@ CreateFloatingIpSideModalForm.loader = async () => { return null } +const toListboxItem = (p: SiloIpPool) => { + if (!p.isDefault) { + return { value: p.name, label: p.name } + } + // For the default pool, add a label to the dropdown + return { + value: p.name, + labelString: p.name, + label: ( + <> + {p.name}{' '} + + default + + + ), + } +} + +const defaultValues: SetRequired = { + name: '', + description: '', + pool: undefined, + address: '', +} + export function CreateFloatingIpSideModalForm() { // Fetch 1000 to we can be sure to get them all. const { data: allPools } = usePrefetchedApiQuery('projectIpPoolList', { query: { limit: 1000 }, }) - const defaultValues: SetRequired = { - name: '', - description: '', - pool: undefined, - address: '', - } - const queryClient = useApiQueryClient() const projectSelector = useProjectSelector() const addToast = useToast() @@ -64,31 +82,7 @@ export function CreateFloatingIpSideModalForm() { }) const form = useForm({ defaultValues }) - - const toListboxItem = (p: SiloIpPool) => { - if (!p.isDefault) { - return { - value: p.name, - label: p.name, - } - } - // For the default pool, add a label to the dropdown - return { - value: p.name, - labelString: p.name, - label: ( - <> - {p.name}{' '} - - default - - - ), - } - } - - const [poolName] = useWatch({ control: form.control, name: ['pool'] }) - const isPoolSelected = poolName && poolName.length > 0 + const isPoolSelected = !!form.watch('pool') return ( Date: Tue, 20 Feb 2024 13:43:02 -0800 Subject: [PATCH 53/64] Accordion to hide advanced settings; refactorable --- app/forms/floating-ip-create.tsx | 98 +++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 22 deletions(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index 45f24fbb26..d259820b01 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -5,6 +5,9 @@ * * Copyright Oxide Computer Company */ +import * as Accordion from '@radix-ui/react-accordion' +import cn from 'classnames' +import { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import type { SetRequired } from 'type-fest' @@ -16,7 +19,7 @@ import { type FloatingIpCreate, type SiloIpPool, } from '@oxide/api' -import { Badge, Divider, Message } from '@oxide/ui' +import { Badge, DirectionRightIcon, Message } from '@oxide/ui' import { validateIp } from '@oxide/util' import { @@ -84,6 +87,8 @@ export function CreateFloatingIpSideModalForm() { const form = useForm({ defaultValues }) const isPoolSelected = !!form.watch('pool') + const [openItems, setOpenItems] = useState([]) + return ( - - - - {/* Todo: collapse these two fields under an "advanced" section */} - toListboxItem(p))} - label="IP pool" - control={form.control} - placeholder="Select pool" - /> - v.replace(/\s/g, '')} - validate={(ip) => (ip && !validateIp(ip).valid ? 'Not a valid IP address' : true)} - /> + + + + + + toListboxItem(p))} + label="IP pool" + control={form.control} + placeholder="Select pool" + /> + v.replace(/\s/g, '')} + validate={(ip) => + ip && !validateIp(ip).valid ? 'Not a valid IP address' : true + } + /> + + ) } + +type AccordionItemProps = { + value: string + isOpen: boolean + label: string + children: React.ReactNode +} + +function AccordionItem({ value, label, children, isOpen }: AccordionItemProps) { + const contentRef = useRef(null) + + useEffect(() => { + if (isOpen && contentRef.current) { + contentRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [isOpen]) + + return ( + + + +
{label}
+ +
+
+ + {children} + +
+ ) +} From 704eadee84cac33c2ed16be343aa3abf9a9f3b4f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 20 Feb 2024 16:40:57 -0800 Subject: [PATCH 54/64] add e2e test for creating a floating ip; attach / detach still in process --- app/test/e2e/floating-ip-create.e2e.ts | 73 ++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 app/test/e2e/floating-ip-create.e2e.ts diff --git a/app/test/e2e/floating-ip-create.e2e.ts b/app/test/e2e/floating-ip-create.e2e.ts new file mode 100644 index 0000000000..6d9fea9f24 --- /dev/null +++ b/app/test/e2e/floating-ip-create.e2e.ts @@ -0,0 +1,73 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { clickRowAction, expect, expectNotVisible, expectVisible, test } from './utils' + +const floatingIpsPage = '/projects/mock-project/floating-ips' + +test('can create a Floating IP', async ({ page }) => { + await page.goto(floatingIpsPage) + await page.locator('text="New Floating IP"').click() + + await expectVisible(page, [ + 'role=heading[name*="Create Floating IP"]', + 'role=textbox[name="Name"]', + 'role=textbox[name="Description"]', + 'role=button[name="Advanced"]', + 'role=button[name="Create Floating IP"]', + ]) + + const floatingIpName = 'my-floating-ip' + await page.fill('input[name=name]', floatingIpName) + await page.fill('textarea[name=description]', 'A description for this Floating IP') + + // accordion content should be hidden + await expectNotVisible(page, ['role=textbox[name="Address"]']) + + // open accordion + await page.getByRole('button', { name: 'Advanced' }).click() + + // accordion content should be visible + await expectVisible(page, ['role=textbox[name="Address"]']) + + await page.getByRole('button', { name: 'Create Floating IP' }).click() + + await expect(page).toHaveURL(floatingIpsPage) + + await expectVisible(page, [ + `text=${floatingIpName}`, + 'text=A description for this Floating IP', + ]) +}) + +test.skip('can detach and attach a Floating IP', async ({ page }) => { + await page.goto(floatingIpsPage) + + await expectVisible(page, ['text=db1']) + await clickRowAction(page, 'cola-float', 'Detach') + + await page.locator('text="Select an instance"').click() + + await expectVisible(page, ['role=heading[name*="Detach Floating IP"]']) + + await page.getByRole('button', { name: 'Confirm' }).click() + await expect(page).toHaveURL(floatingIpsPage) + await expectNotVisible(page, ['text=db1']) + + // Reattach it to db1 + await clickRowAction(page, 'cola-float', 'Attach') + await page.locator('text="Select instance"').click() + // click the down arrow + await page.locator('role=button[name="Instance"]').click() + // click the option + await page.locator('role=option[name="db1"]').click() + await page.getByRole('button', { name: 'Confirm' }).click() + await expect(page).toHaveURL(floatingIpsPage) + + await expectVisible(page, ['text=db1']) +}) From 3773c54ac7b82bbe63526e3e1e14e71917cb9976 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 20 Feb 2024 17:04:46 -0800 Subject: [PATCH 55/64] Attacah/Detach test passes --- app/test/e2e/floating-ip-create.e2e.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/app/test/e2e/floating-ip-create.e2e.ts b/app/test/e2e/floating-ip-create.e2e.ts index 6d9fea9f24..576c8e6662 100644 --- a/app/test/e2e/floating-ip-create.e2e.ts +++ b/app/test/e2e/floating-ip-create.e2e.ts @@ -45,29 +45,27 @@ test('can create a Floating IP', async ({ page }) => { ]) }) -test.skip('can detach and attach a Floating IP', async ({ page }) => { +test('can detach and attach a Floating IP', async ({ page }) => { await page.goto(floatingIpsPage) await expectVisible(page, ['text=db1']) await clickRowAction(page, 'cola-float', 'Detach') - - await page.locator('text="Select an instance"').click() - - await expectVisible(page, ['role=heading[name*="Detach Floating IP"]']) - await page.getByRole('button', { name: 'Confirm' }).click() - await expect(page).toHaveURL(floatingIpsPage) + + await expectNotVisible(page, ['role=heading[name*="Detach Floating IP"]']) + // Since we detached it, we don't expect to see db1 any longer await expectNotVisible(page, ['text=db1']) // Reattach it to db1 await clickRowAction(page, 'cola-float', 'Attach') await page.locator('text="Select instance"').click() - // click the down arrow - await page.locator('role=button[name="Instance"]').click() - // click the option - await page.locator('role=option[name="db1"]').click() - await page.getByRole('button', { name: 'Confirm' }).click() - await expect(page).toHaveURL(floatingIpsPage) + // Click the down arrow and select top option + await page.keyboard.press('ArrowDown') + await page.keyboard.press('Enter') + await page.getByRole('button', { name: 'Attach' }).click() + + // The dialog should be gone + await expectNotVisible(page, ['role=heading[name*="Attach Floating IP"]']) await expectVisible(page, ['text=db1']) }) From 12c2441909f1b3bab70e7be99d6d502559570a24 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 21 Feb 2024 10:52:11 -0800 Subject: [PATCH 56/64] Extract simplified AccordionItem component --- app/components/AccordionItem.tsx | 35 ++++++++++++++++++++++++ app/forms/floating-ip-create.tsx | 47 +++----------------------------- 2 files changed, 39 insertions(+), 43 deletions(-) create mode 100644 app/components/AccordionItem.tsx diff --git a/app/components/AccordionItem.tsx b/app/components/AccordionItem.tsx new file mode 100644 index 0000000000..00e166b459 --- /dev/null +++ b/app/components/AccordionItem.tsx @@ -0,0 +1,35 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import * as Accordion from '@radix-ui/react-accordion' +import cn from 'classnames' + +import { DirectionRightIcon } from '@oxide/design-system/icons/react' + +type AccordionItemProps = { + children: React.ReactNode + label: string + value: string +} + +// This is a simplified AccordionItem component that does not concern itself with scrolling into view when expanded +// See instance-create for a more involved example, including scrolling +export const AccordionItem = ({ children, label, value }: AccordionItemProps) => { + return ( + + + +
{label}
+ +
+
+ + {children} + +
+ ) +} diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index d259820b01..394b49f4c6 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -6,8 +6,7 @@ * Copyright Oxide Computer Company */ import * as Accordion from '@radix-ui/react-accordion' -import cn from 'classnames' -import { useEffect, useRef, useState } from 'react' +import { useState } from 'react' import { useNavigate } from 'react-router-dom' import type { SetRequired } from 'type-fest' @@ -19,9 +18,10 @@ import { type FloatingIpCreate, type SiloIpPool, } from '@oxide/api' -import { Badge, DirectionRightIcon, Message } from '@oxide/ui' +import { Badge, Message } from '@oxide/ui' import { validateIp } from '@oxide/util' +import { AccordionItem } from 'app/components/AccordionItem' import { DescriptionField, ListboxField, @@ -114,11 +114,7 @@ export function CreateFloatingIpSideModalForm() { value={openItems} onValueChange={setOpenItems} > - + ) } - -type AccordionItemProps = { - value: string - isOpen: boolean - label: string - children: React.ReactNode -} - -function AccordionItem({ value, label, children, isOpen }: AccordionItemProps) { - const contentRef = useRef(null) - - useEffect(() => { - if (isOpen && contentRef.current) { - contentRef.current.scrollIntoView({ behavior: 'smooth' }) - } - }, [isOpen]) - - return ( - - - -
{label}
- -
-
- - {children} - -
- ) -} From d5089566231a51f9a74d2ce99ee6b6d7febdddcc Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 21 Feb 2024 11:09:06 -0800 Subject: [PATCH 57/64] Pull advanced props over to AccordionItem so we can reuse it wherever we need it --- app/components/AccordionItem.tsx | 19 +++++++++++---- app/forms/floating-ip-create.tsx | 6 ++++- app/forms/instance-create.tsx | 40 ++------------------------------ 3 files changed, 22 insertions(+), 43 deletions(-) diff --git a/app/components/AccordionItem.tsx b/app/components/AccordionItem.tsx index 00e166b459..2ca660d416 100644 --- a/app/components/AccordionItem.tsx +++ b/app/components/AccordionItem.tsx @@ -7,18 +7,25 @@ */ import * as Accordion from '@radix-ui/react-accordion' import cn from 'classnames' +import { useEffect, useRef } from 'react' import { DirectionRightIcon } from '@oxide/design-system/icons/react' type AccordionItemProps = { children: React.ReactNode + isOpen: boolean label: string value: string } -// This is a simplified AccordionItem component that does not concern itself with scrolling into view when expanded -// See instance-create for a more involved example, including scrolling -export const AccordionItem = ({ children, label, value }: AccordionItemProps) => { +export const AccordionItem = ({ children, isOpen, label, value }: AccordionItemProps) => { + const contentRef = useRef(null) + useEffect(() => { + if (isOpen && contentRef.current) { + contentRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [isOpen]) + return ( @@ -27,7 +34,11 @@ export const AccordionItem = ({ children, label, value }: AccordionItemProps) => - + {children} diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index 394b49f4c6..e46211c8cd 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -114,7 +114,11 @@ export function CreateFloatingIpSideModalForm() { value={openItems} onValueChange={setOpenItems} > - + (null) - - useEffect(() => { - if (isOpen && contentRef.current) { - contentRef.current.scrollIntoView({ behavior: 'smooth' }) - } - }, [isOpen]) - - return ( - - - -
{label}
- -
-
- - {children} - -
- ) -} - const renderLargeRadioCards = (category: string) => { return PRESETS.filter((option) => option.category === category).map((option) => ( From 8b0b87ea32b50eec37a1620f9c7b4f9316c0aa94 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 21 Feb 2024 15:35:02 -0800 Subject: [PATCH 58/64] Updated test with helper utils --- app/test/e2e/floating-ip-create.e2e.ts | 63 ++++++++++++++++++-------- app/test/e2e/utils.ts | 27 +++++++++++ 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/app/test/e2e/floating-ip-create.e2e.ts b/app/test/e2e/floating-ip-create.e2e.ts index 576c8e6662..0d8acbfb16 100644 --- a/app/test/e2e/floating-ip-create.e2e.ts +++ b/app/test/e2e/floating-ip-create.e2e.ts @@ -6,13 +6,25 @@ * Copyright Oxide Computer Company */ -import { clickRowAction, expect, expectNotVisible, expectVisible, test } from './utils' +import { + clearTextbox, + clickButton, + clickLink, + clickListboxItem, + clickRowAction, + expect, + expectNotVisible, + expectRowVisible, + expectVisible, + fillTextbox, + test, +} from './utils' const floatingIpsPage = '/projects/mock-project/floating-ips' test('can create a Floating IP', async ({ page }) => { await page.goto(floatingIpsPage) - await page.locator('text="New Floating IP"').click() + await clickLink(page, 'New Floating IP') await expectVisible(page, [ 'role=heading[name*="Create Floating IP"]', @@ -24,33 +36,48 @@ test('can create a Floating IP', async ({ page }) => { const floatingIpName = 'my-floating-ip' await page.fill('input[name=name]', floatingIpName) - await page.fill('textarea[name=description]', 'A description for this Floating IP') + await fillTextbox(page, 'Description', 'A description for this Floating IP') // accordion content should be hidden await expectNotVisible(page, ['role=textbox[name="Address"]']) // open accordion - await page.getByRole('button', { name: 'Advanced' }).click() + await clickButton(page, 'Advanced') // accordion content should be visible - await expectVisible(page, ['role=textbox[name="Address"]']) + await expectVisible(page, [ + page.getByRole('button', { name: 'IP pool' }), + page.getByRole('textbox', { name: 'Address' }), + ]) + + // test that the IP validation works + await clickListboxItem(page, 'IP pool', 'ip-pool-1') + await fillTextbox(page, 'Address', '256.256.256.256') + await clickButton(page, 'Create Floating IP') + await expect(page.getByText('Not a valid IP address').first()).toBeVisible() + + // correct IP and submit + await clearTextbox(page, 'Address') + await fillTextbox(page, 'Address', '12.34.56.78') - await page.getByRole('button', { name: 'Create Floating IP' }).click() + await clickButton(page, 'Create Floating IP') await expect(page).toHaveURL(floatingIpsPage) - await expectVisible(page, [ - `text=${floatingIpName}`, - 'text=A description for this Floating IP', - ]) + await expectRowVisible(page.getByRole('table'), { + name: floatingIpName, + description: 'A description for this Floating IP', + }) }) test('can detach and attach a Floating IP', async ({ page }) => { await page.goto(floatingIpsPage) - await expectVisible(page, ['text=db1']) + await expectRowVisible(page.getByRole('table'), { + 'Attached to instance': 'db1', + }) await clickRowAction(page, 'cola-float', 'Detach') - await page.getByRole('button', { name: 'Confirm' }).click() + await clickButton(page, 'Confirm') await expectNotVisible(page, ['role=heading[name*="Detach Floating IP"]']) // Since we detached it, we don't expect to see db1 any longer @@ -58,14 +85,12 @@ test('can detach and attach a Floating IP', async ({ page }) => { // Reattach it to db1 await clickRowAction(page, 'cola-float', 'Attach') - await page.locator('text="Select instance"').click() - - // Click the down arrow and select top option - await page.keyboard.press('ArrowDown') - await page.keyboard.press('Enter') - await page.getByRole('button', { name: 'Attach' }).click() + await clickListboxItem(page, 'Select instance', 'db1') + await clickButton(page, 'Attach') // The dialog should be gone await expectNotVisible(page, ['role=heading[name*="Attach Floating IP"]']) - await expectVisible(page, ['text=db1']) + await expectRowVisible(page.getByRole('table'), { + 'Attached to instance': 'db1', + }) }) diff --git a/app/test/e2e/utils.ts b/app/test/e2e/utils.ts index 82ea572010..64c794e2fb 100644 --- a/app/test/e2e/utils.ts +++ b/app/test/e2e/utils.ts @@ -127,6 +127,33 @@ export async function clickRowAction(page: Page, rowText: string, actionName: st await page.getByRole('menuitem', { name: actionName }).click() } +/** Select a Listbox and click the specified option */ +export async function clickListboxItem( + page: Page, + buttonName: string, + optionName: string, + exact?: boolean +) { + await page.getByRole('button', { name: buttonName }).click() + await page.getByRole('option', { name: optionName, exact }).click() +} + +export async function clickButton(page: Page, name: string) { + await page.getByRole('button', { name }).click() +} + +export async function clickLink(page: Page, name: string) { + await page.getByRole('link', { name }).click() +} + +export async function fillTextbox(page: Page, name: string, value: string) { + await page.getByRole('textbox', { name }).fill(value) +} + +export async function clearTextbox(page: Page, name: string) { + await page.getByRole('textbox', { name }).clear() +} + export async function getPageAsUser( browser: Browser, user: 'Hans Jonas' | 'Simone de Beauvoir' From 7868586341e22d81bcfa577b43f99ad65a1a8c46 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 21 Feb 2024 16:30:14 -0800 Subject: [PATCH 59/64] Revert Playwright utils --- app/test/e2e/floating-ip-create.e2e.ts | 33 ++++++++++++-------------- app/test/e2e/utils.ts | 16 ------------- 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/app/test/e2e/floating-ip-create.e2e.ts b/app/test/e2e/floating-ip-create.e2e.ts index 0d8acbfb16..2ca0e5c357 100644 --- a/app/test/e2e/floating-ip-create.e2e.ts +++ b/app/test/e2e/floating-ip-create.e2e.ts @@ -7,16 +7,12 @@ */ import { - clearTextbox, - clickButton, - clickLink, clickListboxItem, clickRowAction, expect, expectNotVisible, expectRowVisible, expectVisible, - fillTextbox, test, } from './utils' @@ -24,7 +20,7 @@ const floatingIpsPage = '/projects/mock-project/floating-ips' test('can create a Floating IP', async ({ page }) => { await page.goto(floatingIpsPage) - await clickLink(page, 'New Floating IP') + await page.locator('text="New Floating IP"').click() await expectVisible(page, [ 'role=heading[name*="Create Floating IP"]', @@ -36,31 +32,32 @@ test('can create a Floating IP', async ({ page }) => { const floatingIpName = 'my-floating-ip' await page.fill('input[name=name]', floatingIpName) - await fillTextbox(page, 'Description', 'A description for this Floating IP') + await page + .getByRole('textbox', { name: 'Description' }) + .fill('A description for this Floating IP') // accordion content should be hidden await expectNotVisible(page, ['role=textbox[name="Address"]']) // open accordion - await clickButton(page, 'Advanced') + await page.getByRole('button', { name: 'Advanced' }).click() + + const addressTextbox = page.getByRole('textbox', { name: 'Address' }) // accordion content should be visible - await expectVisible(page, [ - page.getByRole('button', { name: 'IP pool' }), - page.getByRole('textbox', { name: 'Address' }), - ]) + await expectVisible(page, [page.getByRole('button', { name: 'IP pool' }), addressTextbox]) // test that the IP validation works await clickListboxItem(page, 'IP pool', 'ip-pool-1') - await fillTextbox(page, 'Address', '256.256.256.256') - await clickButton(page, 'Create Floating IP') + await addressTextbox.fill('256.256.256.256') + await page.getByRole('button', { name: 'Create Floating IP' }).click() await expect(page.getByText('Not a valid IP address').first()).toBeVisible() // correct IP and submit - await clearTextbox(page, 'Address') - await fillTextbox(page, 'Address', '12.34.56.78') + await addressTextbox.clear() + await addressTextbox.fill('12.34.56.78') - await clickButton(page, 'Create Floating IP') + await page.getByRole('button', { name: 'Create Floating IP' }).click() await expect(page).toHaveURL(floatingIpsPage) @@ -77,7 +74,7 @@ test('can detach and attach a Floating IP', async ({ page }) => { 'Attached to instance': 'db1', }) await clickRowAction(page, 'cola-float', 'Detach') - await clickButton(page, 'Confirm') + await page.getByRole('button', { name: 'Confirm' }).click() await expectNotVisible(page, ['role=heading[name*="Detach Floating IP"]']) // Since we detached it, we don't expect to see db1 any longer @@ -86,7 +83,7 @@ test('can detach and attach a Floating IP', async ({ page }) => { // Reattach it to db1 await clickRowAction(page, 'cola-float', 'Attach') await clickListboxItem(page, 'Select instance', 'db1') - await clickButton(page, 'Attach') + await page.getByRole('button', { name: 'Attach' }).click() // The dialog should be gone await expectNotVisible(page, ['role=heading[name*="Attach Floating IP"]']) diff --git a/app/test/e2e/utils.ts b/app/test/e2e/utils.ts index 64c794e2fb..8e97e8ec63 100644 --- a/app/test/e2e/utils.ts +++ b/app/test/e2e/utils.ts @@ -138,22 +138,6 @@ export async function clickListboxItem( await page.getByRole('option', { name: optionName, exact }).click() } -export async function clickButton(page: Page, name: string) { - await page.getByRole('button', { name }).click() -} - -export async function clickLink(page: Page, name: string) { - await page.getByRole('link', { name }).click() -} - -export async function fillTextbox(page: Page, name: string, value: string) { - await page.getByRole('textbox', { name }).fill(value) -} - -export async function clearTextbox(page: Page, name: string) { - await page.getByRole('textbox', { name }).clear() -} - export async function getPageAsUser( browser: Browser, user: 'Hans Jonas' | 'Simone de Beauvoir' From 3195eb16966b79ed5165a4b47c4283207a9de1a7 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 21 Feb 2024 16:36:07 -0800 Subject: [PATCH 60/64] Last util cleanup --- app/test/e2e/floating-ip-create.e2e.ts | 8 +++++--- app/test/e2e/utils.ts | 11 ----------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/app/test/e2e/floating-ip-create.e2e.ts b/app/test/e2e/floating-ip-create.e2e.ts index 2ca0e5c357..52ebb07aaa 100644 --- a/app/test/e2e/floating-ip-create.e2e.ts +++ b/app/test/e2e/floating-ip-create.e2e.ts @@ -7,7 +7,6 @@ */ import { - clickListboxItem, clickRowAction, expect, expectNotVisible, @@ -48,7 +47,8 @@ test('can create a Floating IP', async ({ page }) => { await expectVisible(page, [page.getByRole('button', { name: 'IP pool' }), addressTextbox]) // test that the IP validation works - await clickListboxItem(page, 'IP pool', 'ip-pool-1') + await page.getByRole('button', { name: 'IP pool' }).click() + await page.getByRole('option', { name: 'ip-pool-1', exact: true }).click() await addressTextbox.fill('256.256.256.256') await page.getByRole('button', { name: 'Create Floating IP' }).click() await expect(page.getByText('Not a valid IP address').first()).toBeVisible() @@ -82,7 +82,9 @@ test('can detach and attach a Floating IP', async ({ page }) => { // Reattach it to db1 await clickRowAction(page, 'cola-float', 'Attach') - await clickListboxItem(page, 'Select instance', 'db1') + await page.getByRole('button', { name: 'Select instance' }).click() + await page.getByRole('option', { name: 'db1' }).click() + await page.getByRole('button', { name: 'Attach' }).click() // The dialog should be gone diff --git a/app/test/e2e/utils.ts b/app/test/e2e/utils.ts index 8e97e8ec63..82ea572010 100644 --- a/app/test/e2e/utils.ts +++ b/app/test/e2e/utils.ts @@ -127,17 +127,6 @@ export async function clickRowAction(page: Page, rowText: string, actionName: st await page.getByRole('menuitem', { name: actionName }).click() } -/** Select a Listbox and click the specified option */ -export async function clickListboxItem( - page: Page, - buttonName: string, - optionName: string, - exact?: boolean -) { - await page.getByRole('button', { name: buttonName }).click() - await page.getByRole('option', { name: optionName, exact }).click() -} - export async function getPageAsUser( browser: Browser, user: 'Hans Jonas' | 'Simone de Beauvoir' From 0f9dd2fc5c152dbeab2a5a45089f08cf451df270 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 21 Feb 2024 17:05:40 -0800 Subject: [PATCH 61/64] Fix Playwright test --- app/test/e2e/floating-ip-create.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/test/e2e/floating-ip-create.e2e.ts b/app/test/e2e/floating-ip-create.e2e.ts index 52ebb07aaa..50198b47b9 100644 --- a/app/test/e2e/floating-ip-create.e2e.ts +++ b/app/test/e2e/floating-ip-create.e2e.ts @@ -48,7 +48,7 @@ test('can create a Floating IP', async ({ page }) => { // test that the IP validation works await page.getByRole('button', { name: 'IP pool' }).click() - await page.getByRole('option', { name: 'ip-pool-1', exact: true }).click() + await page.getByRole('option', { name: 'ip-pool-1' }).click() await addressTextbox.fill('256.256.256.256') await page.getByRole('button', { name: 'Create Floating IP' }).click() await expect(page.getByText('Not a valid IP address').first()).toBeVisible() From 1f3bf52d8b8eb8c13078eadbc1b8b63ba82471c0 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 22 Feb 2024 11:13:06 -0800 Subject: [PATCH 62/64] Add info box to attach modal; update action menu to have either attach or detach --- .../project/floating-ips/FloatingIpsPage.tsx | 91 ++++++++++--------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx index 1bbf6efb26..e9d5552ceb 100644 --- a/app/pages/project/floating-ips/FloatingIpsPage.tsx +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -23,6 +23,7 @@ import { EmptyMessage, IpGlobal24Icon, Listbox, + Message, Modal, Networking24Icon, PageHeader, @@ -89,47 +90,43 @@ export function FloatingIpsPage() { const makeActions = (floatingIp: FloatingIp): MenuAction[] => { const isAttachedToAnInstance = !!floatingIp.instanceId + const attachOrDetachAction = isAttachedToAnInstance + ? { + label: 'Detach', + onActivate: () => + confirmAction({ + actionType: 'danger', + doAction: () => + floatingIpDetach.mutateAsync({ + path: { floatingIp: floatingIp.name }, + query: { project }, + }), + modalTitle: 'Detach Floating IP', + modalContent: ( +

+ Are you sure you want to detach floating IP {floatingIp.name}{' '} + from instance{' '} + + { + // instanceId is guaranteed to be non-null here + getInstanceName(floatingIp.instanceId!) + } + + ? The instance will no longer be reachable at {floatingIp.ip}. +

+ ), + errorTitle: 'Error detaching floating IP', + }), + } + : { + label: 'Attach', + onActivate() { + setFloatingIpToModify(floatingIp) + setAttachModalOpen(true) + }, + } return [ - { - label: 'Attach', - disabled: isAttachedToAnInstance - ? 'This floating IP must be detached from the existing instance before it can be attached to a new one' - : false, - onActivate() { - setFloatingIpToModify(floatingIp) - setAttachModalOpen(true) - }, - }, - { - label: 'Detach', - disabled: isAttachedToAnInstance - ? false - : 'This floating IP is not attached to an instance', - onActivate: () => - confirmAction({ - actionType: 'danger', - doAction: () => - floatingIpDetach.mutateAsync({ - path: { floatingIp: floatingIp.name }, - query: { project }, - }), - modalTitle: 'Detach Floating IP', - modalContent: ( -

- Are you sure you want to detach the floating IP {floatingIp.name}{' '} - from the instance{' '} - - { - // instanceId is guaranteed to be non-null here - getInstanceName(floatingIp.instanceId!) - } - - ? -

- ), - errorTitle: 'Error detaching floating IP', - }), - }, + attachOrDetachAction, { label: 'Delete', disabled: isAttachedToAnInstance @@ -172,6 +169,7 @@ export function FloatingIpsPage() { {attachModalOpen && floatingIpToModify && ( setAttachModalOpen(false)} @@ -183,11 +181,13 @@ export function FloatingIpsPage() { const AttachFloatingIpModal = ({ floatingIp, + address, instances, project, onDismiss, }: { floatingIp: string + address: string instances: Array project: string onDismiss: () => void @@ -209,12 +209,19 @@ const AttachFloatingIpModal = ({ - {/* Todo: Add help text explaining what selecting an instance will do */} + + The selected instance will be reachable at {address} + + } + >
({ value: i.id, label: i.name }))} - label="Select an instance" + label="Instance" onChange={(e) => { form.setValue('instanceId', e) }} From 2a17795479dde1402bad9512cdfa393162121183 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 22 Feb 2024 11:14:20 -0800 Subject: [PATCH 63/64] No period for single-line Message --- app/forms/floating-ip-create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index e46211c8cd..73d46b44b2 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -121,7 +121,7 @@ export function CreateFloatingIpSideModalForm() { > Date: Thu, 22 Feb 2024 17:05:00 -0800 Subject: [PATCH 64/64] Refactor useState flow to more intelligently use null --- app/pages/project/floating-ips/FloatingIpsPage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx index e9d5552ceb..cad32e09cb 100644 --- a/app/pages/project/floating-ips/FloatingIpsPage.tsx +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -62,7 +62,6 @@ FloatingIpsPage.loader = async ({ params }: LoaderFunctionArgs) => { } export function FloatingIpsPage() { - const [attachModalOpen, setAttachModalOpen] = useState(false) const [floatingIpToModify, setFloatingIpToModify] = useState(null) const queryClient = useApiQueryClient() const { project } = useProjectSelector() @@ -122,7 +121,6 @@ export function FloatingIpsPage() { label: 'Attach', onActivate() { setFloatingIpToModify(floatingIp) - setAttachModalOpen(true) }, } return [ @@ -166,13 +164,13 @@ export function FloatingIpsPage() { /> - {attachModalOpen && floatingIpToModify && ( + {floatingIpToModify && ( setAttachModalOpen(false)} + onDismiss={() => setFloatingIpToModify(null)} /> )}