diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 65d586c405..067484141e 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -5438a551ec03db97f67da170882ba458ed708280 +4cf991a10b8919625f3358fa4e0eb978eeda8da9 diff --git a/app/components/SystemMetric.tsx b/app/components/SystemMetric.tsx index 44ea30396d..e8bde6bc39 100644 --- a/app/components/SystemMetric.tsx +++ b/app/components/SystemMetric.tsx @@ -1,16 +1,16 @@ -// import type { SystemMetricName } from '@oxide/api' -// import { useApiQuery } from '@oxide/api' -import type { MeasurementResultsPage } from '@oxide/api' +import React, { Suspense } from 'react' + +import type { SystemMetricName } from '@oxide/api' +import { useApiQuery } from '@oxide/api' import { Spinner } from '@oxide/ui' -import { TimeSeriesAreaChart } from './TimeSeriesChart' +const TimeSeriesChart = React.lazy(() => import('./TimeSeriesChart')) type SystemMetricProps = { title: string startTime: Date endTime: Date - metricName: string - // metricName: SystemMetricName + metricName: SystemMetricName /** Resource to filter data by. Can be fleet, silo, org, project. */ filterId: string valueTransform?: (n: number) => number @@ -20,22 +20,22 @@ export function SystemMetric({ title, startTime, endTime, + metricName, + filterId, valueTransform = (x) => x, }: SystemMetricProps) { // TODO: we're only pulling the first page. Should we bump the cap to 10k? // Fetch multiple pages if 10k is not enough? That's a bit much. - // const { data: metrics, isLoading } = useApiQuery( - // 'systemMetric', - // { id: filterId, metricName, startTime, endTime }, - // { - // // TODO: this is actually kind of useless unless the time interval slides forward as time passes - // refetchInterval: 5000, - // // avoid graphs flashing blank while loading when you change the time - // keepPreviousData: true, - // } - // ) - const metrics: MeasurementResultsPage = { items: [] } - const isLoading = false + const { data: metrics, isLoading } = useApiQuery( + 'systemMetric', + { path: { metricName }, query: { id: filterId, startTime, endTime } }, + { + // TODO: this is actually kind of useless unless the time interval slides forward as time passes + refetchInterval: 5000, + // avoid graphs flashing blank while loading when you change the time + keepPreviousData: true, + } + ) const data = (metrics?.items || []).map(({ datum, timestamp }) => ({ timestamp: timestamp.getTime(), @@ -65,15 +65,19 @@ export function SystemMetric({

{title} {isLoading && }

- {/* TODO: this is supposed to be full width */} - + {/* TODO: proper skeleton for empty chart */} + }> + + ) } diff --git a/app/components/TimeSeriesChart.tsx b/app/components/TimeSeriesChart.tsx index 76d57aa29d..6dee943b21 100644 --- a/app/components/TimeSeriesChart.tsx +++ b/app/components/TimeSeriesChart.tsx @@ -1,8 +1,9 @@ +import cn from 'classnames' import { format } from 'date-fns' import { - Area, - AreaChart, CartesianGrid, + Line, + LineChart, ResponsiveContainer, Tooltip, XAxis, @@ -19,25 +20,42 @@ function getTicks(data: { timestamp: number }[], n: number): number[] { if (data.length === 0) return [] if (n < 2) throw Error('n must be at least 2 because of the start and end ticks') // bring the last tick in a bit from the end - const maxIdx = data.length > 10 ? Math.floor((data.length - 1) * 0.9) : data.length - 1 + const maxIdx = data.length > 10 ? Math.floor((data.length - 1) * 0.8) : data.length - 1 + const startOffset = Math.floor((data.length - maxIdx) * 0.6) // if there are 4 ticks, their positions are 0/3, 1/3, 2/3, 3/3 (as fractions of maxIdx) - const idxs = new Array(n).fill(0).map((_, i) => Math.floor((maxIdx * i) / (n - 1))) + const idxs = new Array(n) + .fill(0) + .map((_, i) => Math.floor((maxIdx * i) / (n - 1) + startOffset)) return idxs.map((i) => data[i].timestamp) } +/** + * Check if the start and end time are on the same day + * If they are we can omit the day/month in the date time format + */ +function isSameDay(d1: Date, d2: Date) { + return ( + d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate() + ) +} + const shortDateTime = (ts: number) => format(new Date(ts), 'M/d HH:mm') +const shortTime = (ts: number) => format(new Date(ts), 'HH:mm') const longDateTime = (ts: number) => format(new Date(ts), 'MMM d, yyyy HH:mm:ss zz') -// TODO: change these to theme colors so they work in light mode -const LIGHT_GRAY = 'var(--stroke-default)' const GRID_GRAY = 'var(--stroke-secondary)' -const GREEN = 'var(--base-green-500)' +const GREEN_400 = 'var(--theme-accent-400)' +const GREEN_600 = 'var(--theme-accent-600)' +const GREEN_800 = 'var(--theme-accent-800)' // TODO: figure out how to do this with TW classes instead. As far as I can tell // ticks only take direct styling const textMonoMd = { - fontSize: '0.75rem', + fontSize: '0.6875rem', fontFamily: '"GT America Mono", monospace', + fill: 'var(--content-quaternary)', } function renderTooltip(props: TooltipProps) { @@ -50,10 +68,10 @@ function renderTooltip(props: TooltipProps) { } = payload[0] if (!timestamp || !value) return null return ( -
+
{longDateTime(timestamp)}
-
{name}
+
{name}
{value}
{/* TODO: unit on value if relevant */}
@@ -75,45 +93,33 @@ type Props = { width: number height: number interpolation?: 'linear' | 'stepAfter' - customXTicks?: boolean + startTime: Date + endTime: Date } -// Limitations -// - Only one dataset — can't do overlapping area chart yet - -export function TimeSeriesAreaChart({ +export default function TimeSeriesChart({ className, data, title, width, height, interpolation = 'linear', - customXTicks, + startTime, + endTime, }: Props) { return ( - - {/* TODO: stop tooltip being focused by default on pageload if nothing else has been clicked */} - + + ) } diff --git a/app/pages/SiloUtilizationPage.tsx b/app/pages/SiloUtilizationPage.tsx index c7f71795a2..7363ef9661 100644 --- a/app/pages/SiloUtilizationPage.tsx +++ b/app/pages/SiloUtilizationPage.tsx @@ -10,13 +10,13 @@ import { useDateTimeRangePicker } from 'app/components/form' const DEFAULT_SILO_ID = '001de000-5110-4000-8000-000000000000' const ALL_PROJECTS = '|ALL_PROJECTS|' +const toListboxItem = (x: { name: string; id: string }) => ({ label: x.name, value: x.id }) + SiloUtilizationPage.loader = async () => { await apiQueryClient.prefetchQuery('organizationList', {}) return null } -const toListboxItem = (x: { name: string; id: string }) => ({ label: x.name, value: x.id }) - export function SiloUtilizationPage() { // this will come from /session/me const siloId = DEFAULT_SILO_ID diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index feb3753858..a7ff79ba8a 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -1,25 +1,30 @@ -import { useState } from 'react' +import { Suspense, useMemo, useState } from 'react' +import React from 'react' +import type { LoaderFunctionArgs } from 'react-router-dom' import invariant from 'tiny-invariant' -import type { Cumulativeint64, Disk, DiskMetricName } from '@oxide/api' +import type { Cumulativeint64, DiskMetricName } from '@oxide/api' +import { apiQueryClient } from '@oxide/api' import { useApiQuery } from '@oxide/api' import { Listbox, Spinner } from '@oxide/ui' -import { TimeSeriesAreaChart } from 'app/components/TimeSeriesChart' import { useDateTimeRangePicker } from 'app/components/form' -import { useRequiredParams } from 'app/hooks' +import { requireInstanceParams, useRequiredParams } from 'app/hooks' + +const TimeSeriesChart = React.lazy(() => import('app/components/TimeSeriesChart')) type DiskMetricParams = { title: string + unit?: string startTime: Date endTime: Date metricName: DiskMetricName diskParams: { orgName: string; projectName: string; diskName: string } - // TODO: specify bytes or count } function DiskMetric({ title, + unit, startTime, endTime, metricName, @@ -43,35 +48,52 @@ function DiskMetric({ value: (datum.datum as Cumulativeint64).value, })) - // TODO: indicate time zone somewhere. doesn't have to be in the detail view - // in the tooltip. could be just once on the end of the x-axis like GCP - return ( -
-

- {title} {isLoading && } +
+

+ {title} {unit &&
{unit}
} + {isLoading && }

- + }> + +
) } -// The only reason this needs to be its own component instead of inlined into -// MetricsTab is so we can wait to render _after_ we have the disks response, -// which means we can easily set the default selected disk to the first one -function DiskMetrics({ disks }: { disks: Disk[] }) { +// We could figure out how to prefetch the metrics data, but it would be +// annoying because it relies on the default date range, plus there are 5 calls. +// Considering the data is going to be swapped out as soon as they change the +// date range, I'm inclined to punt. + +MetricsTab.loader = async ({ params }: LoaderFunctionArgs) => { + await apiQueryClient.prefetchQuery('instanceDiskList', { + path: requireInstanceParams(params), + }) + return null +} + +export function MetricsTab() { + const instanceParams = useRequiredParams('orgName', 'projectName', 'instanceName') + const { data } = useApiQuery('instanceDiskList', { path: instanceParams }) + const disks = useMemo(() => data?.items || [], [data]) + + // because of prefetch in the loader and because an instance should always + // have a disk, we should never see an empty list here + invariant(disks.length > 0, 'Instance disks list should never be empty') + const { orgName, projectName } = useRequiredParams('orgName', 'projectName') const { startTime, endTime, dateTimeRangePicker } = useDateTimeRangePicker('lastDay') - invariant(disks.length > 0, 'DiskMetrics should not be rendered with zero disks') const [diskName, setDiskName] = useState(disks[0].name) const diskItems = disks.map(({ name }) => ({ label: name, value: name })) @@ -80,6 +102,7 @@ function DiskMetrics({ disks }: { disks: Disk[] }) { return ( <> +

Disk metrics

- {/* TODO: separate "Reads" from "(count)" so we can - a) style them differently in the title, and - b) show "Reads" but not "(count)" in the Tooltip? - */} -
+
{/* 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({