diff --git a/app/components/oxql-metrics/OxqlMetric.tsx b/app/components/oxql-metrics/OxqlMetric.tsx index 5381a920d..c9b824670 100644 --- a/app/components/oxql-metrics/OxqlMetric.tsx +++ b/app/components/oxql-metrics/OxqlMetric.tsx @@ -11,14 +11,15 @@ * https://github.com/oxidecomputer/omicron/tree/main/oximeter/oximeter/schema */ +import { useQuery } from '@tanstack/react-query' import { Children, useEffect, useMemo, useState, type ReactNode } from 'react' import type { LoaderFunctionArgs } from 'react-router' -import { apiQueryClient, useApiQuery } from '@oxide/api' +import { apiq, queryClient } from '@oxide/api' import { CopyCodeModal } from '~/components/CopyCode' import { MoreActionsMenu } from '~/components/MoreActionsMenu' -import { getInstanceSelector } from '~/hooks/use-params' +import { getInstanceSelector, useProjectSelector } from '~/hooks/use-params' import { useMetricsContext } from '~/pages/project/instances/common' import { LearnMore } from '~/ui/lib/CardBlock' import * as Dropdown from '~/ui/lib/DropdownMenu' @@ -37,10 +38,9 @@ import { export async function loader({ params }: LoaderFunctionArgs) { const { project, instance } = getInstanceSelector(params) - await apiQueryClient.prefetchQuery('instanceView', { - path: { instance }, - query: { project }, - }) + await queryClient.prefetchQuery( + apiq('instanceView', { path: { instance }, query: { project } }) + ) return null } @@ -53,10 +53,10 @@ export type OxqlMetricProps = OxqlQuery & { export function OxqlMetric({ title, description, unit, ...queryObj }: OxqlMetricProps) { // only start reloading data once an intial dataset has been loaded const { setIsIntervalPickerEnabled } = useMetricsContext() + const { project } = useProjectSelector() const query = toOxqlStr(queryObj) - const { data: metrics, error } = useApiQuery( - 'systemTimeseriesQuery', - { body: { query } } + const { data: metrics, error } = useQuery( + apiq('timeseriesQuery', { body: { query }, query: { project } }) // avoid graphs flashing blank while loading when you change the time // { placeholderData: (x) => x } ) diff --git a/app/pages/project/instances/MetricsTab.tsx b/app/pages/project/instances/MetricsTab.tsx index a749388ea..e1e09fb2c 100644 --- a/app/pages/project/instances/MetricsTab.tsx +++ b/app/pages/project/instances/MetricsTab.tsx @@ -38,7 +38,7 @@ export default function MetricsTab() { const { intervalPicker } = useIntervalPicker({ enabled: isIntervalPickerEnabled && preset !== 'custom', - isLoading: useIsFetching({ queryKey: ['systemTimeseriesQuery'] }) > 0, + isLoading: useIsFetching({ queryKey: ['timeseriesQuery'] }) > 0, // sliding the range forward is sufficient to trigger a refetch fn: () => onRangeChange(preset), }) diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index ea1425867..6f26058b2 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -1602,12 +1602,25 @@ export const handlers = makeHandlers({ requireFleetViewer(params.cookies) return handleMetrics(params) }, - async systemTimeseriesQuery(params) { - if (Math.random() > 0.95) throw 500 // random failure - requireFleetViewer(params.cookies) + async timeseriesQuery({ body, query }) { + lookup.project(query) // 404 if project doesn't exist + + // We could try to do something analogous to what the API does, namely + // adding on silo and project to the oxql query to make sure only allowed + // data turns up, but since this endpoint is always called from within a + // project and constructed with project IDs, this would be unlikely to catch + // console bugs. + // https://github.com/oxidecomputer/omicron/blob/cf38148d/nexus/src/app/metrics.rs#L154-L179 + + // timeseries queries are slower than most other queries + await delay(1000) + return handleOxqlMetrics(body) + }, + async systemTimeseriesQuery({ cookies, body }) { + requireFleetViewer(cookies) // timeseries queries are slower than most other queries await delay(1000) - return handleOxqlMetrics(params.body) + return handleOxqlMetrics(body) }, siloMetric: handleMetrics, affinityGroupList: ({ query }) => { @@ -1787,7 +1800,6 @@ export const handlers = makeHandlers({ systemTimeseriesSchemaList: NotImplemented, targetReleaseView: NotImplemented, targetReleaseUpdate: NotImplemented, - timeseriesQuery: NotImplemented, userBuiltinList: NotImplemented, userBuiltinView: NotImplemented, }) diff --git a/test/e2e/instance-metrics.e2e.ts b/test/e2e/instance-metrics.e2e.ts new file mode 100644 index 000000000..c5f7bfdef --- /dev/null +++ b/test/e2e/instance-metrics.e2e.ts @@ -0,0 +1,43 @@ +/* + * 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 { expect, test } from '@playwright/test' + +import { getPageAsUser } from './utils' + +test('Click through instance metrics', async ({ page }) => { + await page.goto('/projects/mock-project/instances/db1/metrics/cpu') + + await expect( + page.getByRole('heading', { name: 'CPU Utilization: Running' }) + ).toBeVisible() + await expect(page.getByText('Something went wrong')).toBeHidden() + + await page.getByRole('tab', { name: 'Disk' }).click() + await expect(page).toHaveURL('/projects/mock-project/instances/db1/metrics/disk') + await expect(page.getByRole('heading', { name: 'Disk Reads' })).toBeVisible() + await expect(page.getByText('Something went wrong')).toBeHidden() + + // exact distinguishes from top level "networking" tab + await page.getByRole('tab', { name: 'Network', exact: true }).click() + await expect(page).toHaveURL('/projects/mock-project/instances/db1/metrics/network') + await expect(page.getByRole('heading', { name: 'Bytes Sent' })).toBeVisible() + await expect(page.getByText('Something went wrong')).toBeHidden() +}) + +// TODO: more detailed tests using the dropdowns to change CPU state and disk + +test('Instance metrics work for non-fleet viewer', async ({ browser }) => { + const page = await getPageAsUser(browser, 'Hans Jonas') + await page.goto('/projects/mock-project/instances/db1/metrics/cpu') + await expect( + page.getByRole('heading', { name: 'CPU Utilization: Running' }) + ).toBeVisible() + // we don't want an error, we want the data! + await expect(page.getByText('Something went wrong')).toBeHidden() +})