diff --git a/app/pages/system/CapacityUtilizationPage.tsx b/app/pages/system/CapacityUtilizationPage.tsx
index dbab91491..94dffbfeb 100644
--- a/app/pages/system/CapacityUtilizationPage.tsx
+++ b/app/pages/system/CapacityUtilizationPage.tsx
@@ -7,7 +7,7 @@
*/
import { getLocalTimeZone, now } from '@internationalized/date'
import { useIsFetching } from '@tanstack/react-query'
-import { memo, useMemo, useState } from 'react'
+import { useMemo, useState } from 'react'
import type { SiloResultsPage } from '@oxide/api'
import {
@@ -28,15 +28,14 @@ import {
Table,
Tabs,
} from '@oxide/ui'
-import { bytesToGiB, bytesToTiB, camelCase } from '@oxide/util'
+import { bytesToGiB, bytesToTiB } from '@oxide/util'
import { CapacityMetric, capacityQueryParams } from 'app/components/CapacityMetric'
import { useIntervalPicker } from 'app/components/RefetchIntervalPicker'
import { SystemMetric } from 'app/components/SystemMetric'
import { useDateTimeRangePicker } from 'app/components/form'
-import type { SiloMetric } from './metrics-util'
-import { mergeSiloMetrics } from './metrics-util'
+import { tabularizeSiloMetrics } from './metrics-util'
CapacityUtilizationPage.loader = async () => {
await Promise.all([
@@ -189,54 +188,23 @@ const MetricsTab = ({
)
}
-const usageTableParams = {
- startTime: new Date(0),
- endTime: capacityQueryParams.endTime,
- limit: 1,
- order: 'descending' as const,
-}
-
-const UsageTab = memo(({ silos }: { silos: SiloResultsPage }) => {
- const siloList = silos?.items.map((silo) => ({ name: silo.name, id: silo.id })) || []
-
- const results = useApiQueries('systemMetric', [
- ...siloList.map((silo) => ({
- path: { metricName: 'virtual_disk_space_provisioned' as const },
- query: { ...usageTableParams, silo: silo.name },
- })),
- ...siloList.map((silo) => ({
- path: { metricName: 'ram_provisioned' as const },
- query: { ...usageTableParams, silo: silo.name },
- })),
- ...siloList.map((silo) => ({
- path: { metricName: 'cpus_provisioned' as const },
- query: { ...usageTableParams, silo: silo.name },
- })),
- ])
+function UsageTab({ silos }: { silos: SiloResultsPage }) {
+ const results = useApiQueries(
+ 'systemMetric',
+ silos.items.flatMap((silo) => {
+ const query = { ...capacityQueryParams, silo: silo.name }
+ return [
+ { path: { metricName: 'virtual_disk_space_provisioned' as const }, query },
+ { path: { metricName: 'ram_provisioned' as const }, query },
+ { path: { metricName: 'cpus_provisioned' as const }, query },
+ ]
+ })
+ )
// TODO: loading state, this could take some time
if (results.some((result) => result.isPending)) return null
- const siloResults = results
- .map((result) => {
- if (result.data && result.data.params) {
- const params = result.data.params
-
- if (!params.query) {
- return undefined
- }
-
- return {
- siloName: params.query.silo,
- metrics: {
- [camelCase(params.path.metricName)]: result.data.items[0].datum.datum,
- },
- } as SiloMetric
- }
- })
- .filter((item): item is SiloMetric => Boolean(item))
-
- const mergedResults = mergeSiloMetrics(siloResults)
+ const mergedResults = tabularizeSiloMetrics(results)
return (
@@ -256,13 +224,13 @@ const UsageTab = memo(({ silos }: { silos: SiloResultsPage }) => {
{mergedResults.map((result) => (
{result.siloName}
- {result.metrics.cpusProvisioned}
+ {result.metrics.cpus_provisioned}
- {bytesToTiB(result.metrics.virtualDiskSpaceProvisioned)}
+ {bytesToTiB(result.metrics.virtual_disk_space_provisioned)}
TiB
- {bytesToGiB(result.metrics.ramProvisioned)}
+ {bytesToGiB(result.metrics.ram_provisioned)}
GiB
@@ -270,4 +238,4 @@ const UsageTab = memo(({ silos }: { silos: SiloResultsPage }) => {
)
-})
+}
diff --git a/app/pages/system/metrics-util.spec.ts b/app/pages/system/metrics-util.spec.ts
index 8596dcf35..276654047 100644
--- a/app/pages/system/metrics-util.spec.ts
+++ b/app/pages/system/metrics-util.spec.ts
@@ -7,27 +7,59 @@
*/
import { expect, test } from 'vitest'
-import { mergeSiloMetrics } from './metrics-util'
+import type { SystemMetricName } from '@oxide/api'
-test('mergeSiloMetrics', () => {
- expect(mergeSiloMetrics([])).toEqual([])
- expect(mergeSiloMetrics([{ siloName: 'a', metrics: { m1: 1 } }])).toEqual([
- { siloName: 'a', metrics: { m1: 1 } },
- ])
- expect(
- mergeSiloMetrics([
- { siloName: 'a', metrics: { m1: 1 } },
- { siloName: 'a', metrics: { m2: 2 } },
- ])
- ).toEqual([{ siloName: 'a', metrics: { m1: 1, m2: 2 } }])
+import type { MetricsResult } from './metrics-util'
+import { tabularizeSiloMetrics } from './metrics-util'
+
+function makeResult(
+ silo: string,
+ metricName: SystemMetricName,
+ value: number
+): MetricsResult {
+ return {
+ data: {
+ items: [
+ {
+ datum: { type: 'i64', datum: value },
+ timestamp: new Date(),
+ },
+ ],
+ params: { query: { silo }, path: { metricName } },
+ },
+ }
+}
+
+test('tabularizeSiloMetrics', () => {
+ expect(tabularizeSiloMetrics([])).toEqual([])
expect(
- mergeSiloMetrics([
- { siloName: 'a', metrics: { m1: 1 } },
- { siloName: 'a', metrics: { m2: 2 } },
- { siloName: 'b', metrics: { m1: 3 } },
+ tabularizeSiloMetrics([
+ makeResult('a', 'virtual_disk_space_provisioned', 1),
+ makeResult('b', 'virtual_disk_space_provisioned', 2),
+ makeResult('a', 'cpus_provisioned', 3),
+ makeResult('b', 'cpus_provisioned', 4),
+ makeResult('a', 'ram_provisioned', 5),
+ makeResult('b', 'ram_provisioned', 6),
+ // here to make sure it gets ignored and doesn't break anything
+ // @ts-expect-error
+ { error: 'whoops' },
])
).toEqual([
- { siloName: 'a', metrics: { m1: 1, m2: 2 } },
- { siloName: 'b', metrics: { m1: 3 } },
+ {
+ siloName: 'a',
+ metrics: {
+ virtual_disk_space_provisioned: 1,
+ cpus_provisioned: 3,
+ ram_provisioned: 5,
+ },
+ },
+ {
+ siloName: 'b',
+ metrics: {
+ virtual_disk_space_provisioned: 2,
+ cpus_provisioned: 4,
+ ram_provisioned: 6,
+ },
+ },
])
})
diff --git a/app/pages/system/metrics-util.ts b/app/pages/system/metrics-util.ts
index fedcd1aa4..941bd106c 100644
--- a/app/pages/system/metrics-util.ts
+++ b/app/pages/system/metrics-util.ts
@@ -5,16 +5,46 @@
*
* Copyright Oxide Computer Company
*/
+import type {
+ MeasurementResultsPage,
+ SystemMetricName,
+ SystemMetricPathParams,
+ SystemMetricQueryParams,
+} from '@oxide/api'
import { groupBy } from '@oxide/util'
-export type SiloMetric = {
+export type MetricsResult = {
+ data?: MeasurementResultsPage & {
+ params: {
+ path: SystemMetricPathParams
+ query?: SystemMetricQueryParams
+ }
+ }
+}
+
+type SiloMetric = {
siloName: string
- metrics: Record
+ metrics: Record
}
-export const mergeSiloMetrics = (results: SiloMetric[]): SiloMetric[] => {
- return groupBy(results, (result) => result.siloName).map(([siloName, values]) => ({
- siloName,
- metrics: Object.assign({}, ...values.map((v) => v.metrics)),
- }))
+/**
+ * Turn the big list of query results into something we can display in a table.
+ * See the tests for an example.
+ */
+export function tabularizeSiloMetrics(results: MetricsResult[]): SiloMetric[] {
+ const processed = results
+ // filter mostly to ensure we don't try to pull data off an error response
+ .filter((r) => r.data?.params.query?.silo)
+ .map((r) => {
+ const metricName = r.data!.params.path.metricName
+ const value = r.data!.items[0].datum.datum as number
+ return {
+ siloName: r.data!.params.query!.silo!,
+ metrics: { [metricName]: value },
+ }
+ })
+
+ return groupBy(processed, (r) => r.siloName).map(([siloName, results]) => {
+ return { siloName, metrics: Object.assign({}, ...results.map((r) => r.metrics)) }
+ })
}