+
{/* see the following link for the source of truth on what these mean
https://github.com/oxidecomputer/crucible/blob/258f162b/upstairs/src/stats.rs#L9-L50 */}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
>
)
}
-
-// spinner should be temporary. wrapping div is to get left alignment
-const Loading = () => (
-
-
-
-)
-
-export default function MetricsTab() {
- const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName')
- const { data: disks } = useApiQuery('instanceDiskList', { path: instanceParams })
-
- return (
- <>
-
Disk metrics
- {disks && disks.items.length > 0 ?
:
}
- >
- )
-}
diff --git a/app/routes.tsx b/app/routes.tsx
index ae660d4dca..6382d8b5ca 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -1,4 +1,3 @@
-import React, { Suspense } from 'react'
import { Navigate, Route, createRoutesFromElements } from 'react-router-dom'
import { RouterDataErrorBoundary } from './components/ErrorBoundary'
@@ -31,6 +30,7 @@ import { OrgAccessPage } from './pages/OrgAccessPage'
import OrgsPage from './pages/OrgsPage'
import ProjectsPage from './pages/ProjectsPage'
import { SiloAccessPage } from './pages/SiloAccessPage'
+import { SiloUtilizationPage } from './pages/SiloUtilizationPage'
import {
DisksPage,
ImagesPage,
@@ -41,19 +41,17 @@ import {
VpcPage,
VpcsPage,
} from './pages/project'
+import { MetricsTab } from './pages/project/instances/instance/tabs/MetricsTab'
import { NetworkingTab } from './pages/project/instances/instance/tabs/NetworkingTab'
import { SerialConsoleTab } from './pages/project/instances/instance/tabs/SerialConsoleTab'
import { StorageTab } from './pages/project/instances/instance/tabs/StorageTab'
import { ProfilePage } from './pages/settings/ProfilePage'
import { SSHKeysPage } from './pages/settings/SSHKeysPage'
+import { CapacityUtilizationPage } from './pages/system/CapacityUtilizationPage'
import { SiloPage } from './pages/system/SiloPage'
import SilosPage from './pages/system/SilosPage'
import { pb } from './util/path-builder'
-const MetricsTab = React.lazy(
- () => import('./pages/project/instances/instance/tabs/MetricsTab')
-)
-
const orgCrumb: CrumbFunc = (m) => m.params.orgName!
const projectCrumb: CrumbFunc = (m) => m.params.projectName!
const instanceCrumb: CrumbFunc = (m) => m.params.instanceName!
@@ -96,7 +94,11 @@ export const routes = createRoutesFromElements(
} />
-
+
}
+ loader={CapacityUtilizationPage.loader}
+ />
@@ -114,7 +116,11 @@ export const routes = createRoutesFromElements(
/>
}>
-
+
}
+ loader={SiloUtilizationPage.loader}
+ />
} loader={OrgsPage.loader}>
-
-
- }
+ element={}
+ loader={MetricsTab.loader}
handle={{ crumb: 'metrics' }}
/>
{
+ // const result = ZVal.ResourceName.safeParse(req.params.resourceName)
+ // if (!result.success) return res(notFoundErr)
+ // const resourceName = result.data
+
+ const cap = params.path.metricName === 'cpus_provisioned' ? 3000 : 4000000000000
+
+ // note we're ignoring the required id query param. since the data is fake
+ // it wouldn't matter, though we should probably 400 if it's missing
+
+ const { startTime, endTime } = getStartAndEndTime(params.query)
+
+ if (endTime <= startTime) return { items: [] }
+
+ return {
+ items: genI64Data(
+ new Array(1000).fill(0).map((x, i) => Math.floor(Math.tanh(i / 500) * cap)),
+ startTime,
+ endTime
+ ),
+ }
+ },
+
diskViewById: lookupById(db.disks),
imageViewById: lookupById(db.images),
instanceNetworkInterfaceViewById: lookupById(db.networkInterfaces),
@@ -990,4 +1013,12 @@ export const handlers = makeHandlers({
projectPolicyViewV1: NotImplemented,
projectUpdateV1: NotImplemented,
projectViewV1: NotImplemented,
+
+ diskListV1: NotImplemented,
+ diskCreateV1: NotImplemented,
+ diskViewV1: NotImplemented,
+ diskDeleteV1: NotImplemented,
+ instanceDiskListV1: NotImplemented,
+ instanceDiskAttachV1: NotImplemented,
+ instanceDiskDetachV1: NotImplemented,
})
diff --git a/libs/api/__generated__/Api.ts b/libs/api/__generated__/Api.ts
index a394bf30f0..10065094b7 100644
--- a/libs/api/__generated__/Api.ts
+++ b/libs/api/__generated__/Api.ts
@@ -251,10 +251,14 @@ export type DiskCreate = {
}
/**
- * Parameters for the {@link Disk} to be attached or detached to an instance
+ * TODO-v1: Delete this Parameters for the {@link Disk} to be attached or detached to an instance
*/
export type DiskIdentifier = { name: Name }
+export type NameOrId = string | Name
+
+export type DiskPath = { disk: NameOrId }
+
/**
* A single page of results
*/
@@ -1767,7 +1771,10 @@ export type DiskMetricName =
| 'write'
| 'write_bytes'
-export type NameOrId = string | Name
+export type SystemMetricName =
+ | 'virtual_disk_space_provisioned'
+ | 'cpus_provisioned'
+ | 'ram_provisioned'
export interface DiskViewByIdPathParams {
id: string
@@ -2431,6 +2438,18 @@ export interface IpPoolServiceRangeListQueryParams {
pageToken?: string
}
+export interface SystemMetricPathParams {
+ metricName: SystemMetricName
+}
+
+export interface SystemMetricQueryParams {
+ endTime?: Date
+ id?: string
+ limit?: number
+ pageToken?: string
+ startTime?: Date
+}
+
export interface SagaListQueryParams {
limit?: number
pageToken?: string
@@ -2532,12 +2551,43 @@ export interface UserListQueryParams {
sortBy?: IdSortMode
}
+export interface DiskListV1QueryParams {
+ limit?: number
+ organization?: NameOrId
+ pageToken?: string
+ project?: NameOrId
+ sortBy?: NameOrIdSortMode
+}
+
+export interface DiskCreateV1QueryParams {
+ organization?: NameOrId
+ project?: NameOrId
+}
+
+export interface DiskViewV1PathParams {
+ disk: NameOrId
+}
+
+export interface DiskViewV1QueryParams {
+ organization?: NameOrId
+ project?: NameOrId
+}
+
+export interface DiskDeleteV1PathParams {
+ disk: NameOrId
+}
+
+export interface DiskDeleteV1QueryParams {
+ organization?: NameOrId
+ project?: NameOrId
+}
+
export interface InstanceListV1QueryParams {
limit?: number
organization?: NameOrId
pageToken?: string
project?: NameOrId
- sortBy?: NameSortMode
+ sortBy?: NameOrIdSortMode
}
export interface InstanceCreateV1QueryParams {
@@ -2563,6 +2613,36 @@ export interface InstanceDeleteV1QueryParams {
project?: NameOrId
}
+export interface InstanceDiskListV1PathParams {
+ instance: NameOrId
+}
+
+export interface InstanceDiskListV1QueryParams {
+ limit?: number
+ organization?: NameOrId
+ pageToken?: string
+ project?: NameOrId
+ sortBy?: NameOrIdSortMode
+}
+
+export interface InstanceDiskAttachV1PathParams {
+ instance: NameOrId
+}
+
+export interface InstanceDiskAttachV1QueryParams {
+ organization?: NameOrId
+ project?: NameOrId
+}
+
+export interface InstanceDiskDetachV1PathParams {
+ instance: NameOrId
+}
+
+export interface InstanceDiskDetachV1QueryParams {
+ organization?: NameOrId
+ project?: NameOrId
+}
+
export interface InstanceMigrateV1PathParams {
instance: NameOrId
}
@@ -3200,7 +3280,7 @@ export class Api extends HttpClient {
})
},
/**
- * Create a disk
+ * Use `POST /v1/disks` instead
*/
diskCreate: (
{ path, body }: { path: DiskCreatePathParams; body: DiskCreate },
@@ -3226,7 +3306,7 @@ export class Api extends HttpClient {
})
},
/**
- * Delete a disk
+ * Use `DELETE /v1/disks/{disk}` instead
*/
diskDelete: ({ path }: { path: DiskDeletePathParams }, params: RequestParams = {}) => {
const { diskName, orgName, projectName } = path
@@ -4483,6 +4563,24 @@ export class Api extends HttpClient {
...params,
})
},
+ /**
+ * Access metrics data
+ */
+ systemMetric: (
+ {
+ path,
+ query = {},
+ }: { path: SystemMetricPathParams; query?: SystemMetricQueryParams },
+ params: RequestParams = {}
+ ) => {
+ const { metricName } = path
+ return this.request({
+ path: `/system/metrics/${metricName}`,
+ method: 'GET',
+ query,
+ ...params,
+ })
+ },
/**
* Fetch the top-level IAM policy
*/
@@ -4803,6 +4901,68 @@ export class Api extends HttpClient {
...params,
})
},
+ /**
+ * List disks
+ */
+ diskListV1: (
+ { query = {} }: { query?: DiskListV1QueryParams },
+ params: RequestParams = {}
+ ) => {
+ return this.request({
+ path: `/v1/disks`,
+ method: 'GET',
+ query,
+ ...params,
+ })
+ },
+ /**
+ * Create a disk
+ */
+ diskCreateV1: (
+ { query = {}, body }: { query?: DiskCreateV1QueryParams; body: DiskCreate },
+ params: RequestParams = {}
+ ) => {
+ return this.request({
+ path: `/v1/disks`,
+ method: 'POST',
+ body,
+ query,
+ ...params,
+ })
+ },
+ /**
+ * Fetch a disk
+ */
+ diskViewV1: (
+ { path, query = {} }: { path: DiskViewV1PathParams; query?: DiskViewV1QueryParams },
+ params: RequestParams = {}
+ ) => {
+ const { disk } = path
+ return this.request({
+ path: `/v1/disks/${disk}`,
+ method: 'GET',
+ query,
+ ...params,
+ })
+ },
+ /**
+ * Delete a disk
+ */
+ diskDeleteV1: (
+ {
+ path,
+ query = {},
+ }: { path: DiskDeleteV1PathParams; query?: DiskDeleteV1QueryParams },
+ params: RequestParams = {}
+ ) => {
+ const { disk } = path
+ return this.request({
+ path: `/v1/disks/${disk}`,
+ method: 'DELETE',
+ query,
+ ...params,
+ })
+ },
/**
* List instances
*/
@@ -4868,6 +5028,63 @@ export class Api extends HttpClient {
...params,
})
},
+ instanceDiskListV1: (
+ {
+ path,
+ query = {},
+ }: { path: InstanceDiskListV1PathParams; query?: InstanceDiskListV1QueryParams },
+ params: RequestParams = {}
+ ) => {
+ const { instance } = path
+ return this.request({
+ path: `/v1/instances/${instance}/disks`,
+ method: 'GET',
+ query,
+ ...params,
+ })
+ },
+ instanceDiskAttachV1: (
+ {
+ path,
+ query = {},
+ body,
+ }: {
+ path: InstanceDiskAttachV1PathParams
+ query?: InstanceDiskAttachV1QueryParams
+ body: DiskPath
+ },
+ params: RequestParams = {}
+ ) => {
+ const { instance } = path
+ return this.request({
+ path: `/v1/instances/${instance}/disks/attach`,
+ method: 'POST',
+ body,
+ query,
+ ...params,
+ })
+ },
+ instanceDiskDetachV1: (
+ {
+ path,
+ query = {},
+ body,
+ }: {
+ path: InstanceDiskDetachV1PathParams
+ query?: InstanceDiskDetachV1QueryParams
+ body: DiskPath
+ },
+ params: RequestParams = {}
+ ) => {
+ const { instance } = path
+ return this.request({
+ path: `/v1/instances/${instance}/disks/detach`,
+ method: 'POST',
+ body,
+ query,
+ ...params,
+ })
+ },
/**
* Migrate an instance
*/
diff --git a/libs/api/__generated__/OMICRON_VERSION b/libs/api/__generated__/OMICRON_VERSION
index 249c7f45fe..f85a91a0ec 100644
--- a/libs/api/__generated__/OMICRON_VERSION
+++ b/libs/api/__generated__/OMICRON_VERSION
@@ -1,2 +1,2 @@
# generated file. do not update manually. see docs/update-pinned-api.md
-5438a551ec03db97f67da170882ba458ed708280
+4cf991a10b8919625f3358fa4e0eb978eeda8da9
diff --git a/libs/api/__generated__/msw-handlers.ts b/libs/api/__generated__/msw-handlers.ts
index 3fff334fd0..f0fca781e3 100644
--- a/libs/api/__generated__/msw-handlers.ts
+++ b/libs/api/__generated__/msw-handlers.ts
@@ -507,6 +507,11 @@ export interface MSWHandlers {
}) => HandlerResult
/** `POST /system/ip-pools-service/ranges/remove` */
ipPoolServiceRangeRemove: (params: { body: Json }) => StatusCode
+ /** `GET /system/metrics/:metricName` */
+ systemMetric: (params: {
+ path: Api.SystemMetricPathParams
+ query: Api.SystemMetricQueryParams
+ }) => HandlerResult
/** `GET /system/policy` */
systemPolicyView: () => HandlerResult
/** `PUT /system/policy` */
@@ -589,6 +594,25 @@ export interface MSWHandlers {
userList: (params: {
query: Api.UserListQueryParams
}) => HandlerResult
+ /** `GET /v1/disks` */
+ diskListV1: (params: {
+ query: Api.DiskListV1QueryParams
+ }) => HandlerResult
+ /** `POST /v1/disks` */
+ diskCreateV1: (params: {
+ query: Api.DiskCreateV1QueryParams
+ body: Json
+ }) => HandlerResult
+ /** `GET /v1/disks/:disk` */
+ diskViewV1: (params: {
+ path: Api.DiskViewV1PathParams
+ query: Api.DiskViewV1QueryParams
+ }) => HandlerResult
+ /** `DELETE /v1/disks/:disk` */
+ diskDeleteV1: (params: {
+ path: Api.DiskDeleteV1PathParams
+ query: Api.DiskDeleteV1QueryParams
+ }) => StatusCode
/** `GET /v1/instances` */
instanceListV1: (params: {
query: Api.InstanceListV1QueryParams
@@ -608,6 +632,23 @@ export interface MSWHandlers {
path: Api.InstanceDeleteV1PathParams
query: Api.InstanceDeleteV1QueryParams
}) => StatusCode
+ /** `GET /v1/instances/:instance/disks` */
+ instanceDiskListV1: (params: {
+ path: Api.InstanceDiskListV1PathParams
+ query: Api.InstanceDiskListV1QueryParams
+ }) => HandlerResult
+ /** `POST /v1/instances/:instance/disks/attach` */
+ instanceDiskAttachV1: (params: {
+ path: Api.InstanceDiskAttachV1PathParams
+ query: Api.InstanceDiskAttachV1QueryParams
+ body: Json
+ }) => HandlerResult
+ /** `POST /v1/instances/:instance/disks/detach` */
+ instanceDiskDetachV1: (params: {
+ path: Api.InstanceDiskDetachV1PathParams
+ query: Api.InstanceDiskDetachV1QueryParams
+ body: Json
+ }) => HandlerResult
/** `POST /v1/instances/:instance/migrate` */
instanceMigrateV1: (params: {
path: Api.InstanceMigrateV1PathParams
@@ -1342,6 +1383,10 @@ export function makeHandlers(handlers: MSWHandlers): RestHandler[] {
'/system/ip-pools-service/ranges/remove',
handler(handlers['ipPoolServiceRangeRemove'], null, schema.IpRange)
),
+ rest.get(
+ '/system/metrics/:metricName',
+ handler(handlers['systemMetric'], schema.SystemMetricParams, null)
+ ),
rest.get('/system/policy', handler(handlers['systemPolicyView'], null, null)),
rest.put(
'/system/policy',
@@ -1440,6 +1485,19 @@ export function makeHandlers(handlers: MSWHandlers): RestHandler[] {
handler(handlers['timeseriesSchemaGet'], schema.TimeseriesSchemaGetParams, null)
),
rest.get('/users', handler(handlers['userList'], schema.UserListParams, null)),
+ rest.get('/v1/disks', handler(handlers['diskListV1'], schema.DiskListV1Params, null)),
+ rest.post(
+ '/v1/disks',
+ handler(handlers['diskCreateV1'], schema.DiskCreateV1Params, schema.DiskCreate)
+ ),
+ rest.get(
+ '/v1/disks/:disk',
+ handler(handlers['diskViewV1'], schema.DiskViewV1Params, null)
+ ),
+ rest.delete(
+ '/v1/disks/:disk',
+ handler(handlers['diskDeleteV1'], schema.DiskDeleteV1Params, null)
+ ),
rest.get(
'/v1/instances',
handler(handlers['instanceListV1'], schema.InstanceListV1Params, null)
@@ -1460,6 +1518,26 @@ export function makeHandlers(handlers: MSWHandlers): RestHandler[] {
'/v1/instances/:instance',
handler(handlers['instanceDeleteV1'], schema.InstanceDeleteV1Params, null)
),
+ rest.get(
+ '/v1/instances/:instance/disks',
+ handler(handlers['instanceDiskListV1'], schema.InstanceDiskListV1Params, null)
+ ),
+ rest.post(
+ '/v1/instances/:instance/disks/attach',
+ handler(
+ handlers['instanceDiskAttachV1'],
+ schema.InstanceDiskAttachV1Params,
+ schema.DiskPath
+ )
+ ),
+ rest.post(
+ '/v1/instances/:instance/disks/detach',
+ handler(
+ handlers['instanceDiskDetachV1'],
+ schema.InstanceDiskDetachV1Params,
+ schema.DiskPath
+ )
+ ),
rest.post(
'/v1/instances/:instance/migrate',
handler(
diff --git a/libs/api/__generated__/validate.ts b/libs/api/__generated__/validate.ts
index 53440874d7..409c8e7f83 100644
--- a/libs/api/__generated__/validate.ts
+++ b/libs/api/__generated__/validate.ts
@@ -280,10 +280,17 @@ export const DiskCreate = z.preprocess(
)
/**
- * Parameters for the {@link Disk} to be attached or detached to an instance
+ * TODO-v1: Delete this Parameters for the {@link Disk} to be attached or detached to an instance
*/
export const DiskIdentifier = z.preprocess(processResponseBody, z.object({ name: Name }))
+export const NameOrId = z.preprocess(
+ processResponseBody,
+ z.union([z.string().uuid(), Name])
+)
+
+export const DiskPath = z.preprocess(processResponseBody, z.object({ disk: NameOrId }))
+
/**
* A single page of results
*/
@@ -1793,9 +1800,9 @@ export const DiskMetricName = z.preprocess(
z.enum(['activated', 'flush', 'read', 'read_bytes', 'write', 'write_bytes'])
)
-export const NameOrId = z.preprocess(
+export const SystemMetricName = z.preprocess(
processResponseBody,
- z.union([z.string().uuid(), Name])
+ z.enum(['virtual_disk_space_provisioned', 'cpus_provisioned', 'ram_provisioned'])
)
export const DiskViewByIdParams = z.preprocess(
@@ -3192,6 +3199,22 @@ export const IpPoolServiceRangeRemoveParams = z.preprocess(
})
)
+export const SystemMetricParams = z.preprocess(
+ processResponseBody,
+ z.object({
+ path: z.object({
+ metricName: SystemMetricName,
+ }),
+ query: z.object({
+ endTime: DateType.optional(),
+ id: z.string().uuid().optional(),
+ limit: z.number().min(1).max(4294967295).optional(),
+ pageToken: z.string().optional(),
+ startTime: DateType.optional(),
+ }),
+ })
+)
+
export const SystemPolicyViewParams = z.preprocess(
processResponseBody,
z.object({
@@ -3435,6 +3458,57 @@ export const UserListParams = z.preprocess(
})
)
+export const DiskListV1Params = z.preprocess(
+ processResponseBody,
+ z.object({
+ path: z.object({}),
+ query: z.object({
+ limit: z.number().min(1).max(4294967295).optional(),
+ organization: NameOrId.optional(),
+ pageToken: z.string().optional(),
+ project: NameOrId.optional(),
+ sortBy: NameOrIdSortMode.optional(),
+ }),
+ })
+)
+
+export const DiskCreateV1Params = z.preprocess(
+ processResponseBody,
+ z.object({
+ path: z.object({}),
+ query: z.object({
+ organization: NameOrId.optional(),
+ project: NameOrId.optional(),
+ }),
+ })
+)
+
+export const DiskViewV1Params = z.preprocess(
+ processResponseBody,
+ z.object({
+ path: z.object({
+ disk: NameOrId,
+ }),
+ query: z.object({
+ organization: NameOrId.optional(),
+ project: NameOrId.optional(),
+ }),
+ })
+)
+
+export const DiskDeleteV1Params = z.preprocess(
+ processResponseBody,
+ z.object({
+ path: z.object({
+ disk: NameOrId,
+ }),
+ query: z.object({
+ organization: NameOrId.optional(),
+ project: NameOrId.optional(),
+ }),
+ })
+)
+
export const InstanceListV1Params = z.preprocess(
processResponseBody,
z.object({
@@ -3444,7 +3518,7 @@ export const InstanceListV1Params = z.preprocess(
organization: NameOrId.optional(),
pageToken: z.string().optional(),
project: NameOrId.optional(),
- sortBy: NameSortMode.optional(),
+ sortBy: NameOrIdSortMode.optional(),
}),
})
)
@@ -3486,6 +3560,48 @@ export const InstanceDeleteV1Params = z.preprocess(
})
)
+export const InstanceDiskListV1Params = z.preprocess(
+ processResponseBody,
+ z.object({
+ path: z.object({
+ instance: NameOrId,
+ }),
+ query: z.object({
+ limit: z.number().min(1).max(4294967295).optional(),
+ organization: NameOrId.optional(),
+ pageToken: z.string().optional(),
+ project: NameOrId.optional(),
+ sortBy: NameOrIdSortMode.optional(),
+ }),
+ })
+)
+
+export const InstanceDiskAttachV1Params = z.preprocess(
+ processResponseBody,
+ z.object({
+ path: z.object({
+ instance: NameOrId,
+ }),
+ query: z.object({
+ organization: NameOrId.optional(),
+ project: NameOrId.optional(),
+ }),
+ })
+)
+
+export const InstanceDiskDetachV1Params = z.preprocess(
+ processResponseBody,
+ z.object({
+ path: z.object({
+ instance: NameOrId,
+ }),
+ query: z.object({
+ organization: NameOrId.optional(),
+ project: NameOrId.optional(),
+ }),
+ })
+)
+
export const InstanceMigrateV1Params = z.preprocess(
processResponseBody,
z.object({