diff --git a/frontend/src/api/prometheus/serving.ts b/frontend/src/api/prometheus/serving.ts index 2f0df96cd8..c868cd1527 100644 --- a/frontend/src/api/prometheus/serving.ts +++ b/frontend/src/api/prometheus/serving.ts @@ -5,7 +5,9 @@ import { RuntimeMetricType, } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; import { MetricType, TimeframeTitle } from '~/pages/modelServing/screens/types'; -import useQueryRangeResourceData from './useQueryRangeResourceData'; +import useQueryRangeResourceData, { + useQueryRangeResourceDataTrusty, +} from './useQueryRangeResourceData'; export const useModelServingMetrics = ( type: MetricType, @@ -61,6 +63,20 @@ export const useModelServingMetrics = ( timeframe, ); + const inferenceTrustyAISPD = useQueryRangeResourceDataTrusty( + type === 'inference', + queries[InferenceMetricType.TRUSTY_AI_SPD], + end, + timeframe, + ); + + const inferenceTrustyAIDIR = useQueryRangeResourceDataTrusty( + type === 'inference', + queries[InferenceMetricType.TRUSTY_AI_DIR], + end, + timeframe, + ); + React.useEffect(() => { setLastUpdateTime(Date.now()); // re-compute lastUpdateTime when data changes @@ -87,6 +103,8 @@ export const useModelServingMetrics = ( [RuntimeMetricType.MEMORY_UTILIZATION]: runtimeMemoryUtilization, [InferenceMetricType.REQUEST_COUNT_SUCCESS]: inferenceRequestSuccessCount, [InferenceMetricType.REQUEST_COUNT_FAILED]: inferenceRequestFailedCount, + [InferenceMetricType.TRUSTY_AI_SPD]: inferenceTrustyAISPD, + [InferenceMetricType.TRUSTY_AI_DIR]: inferenceTrustyAIDIR, }, refresh: refreshAllMetrics, }), @@ -97,6 +115,8 @@ export const useModelServingMetrics = ( runtimeMemoryUtilization, inferenceRequestSuccessCount, inferenceRequestFailedCount, + inferenceTrustyAISPD, + inferenceTrustyAIDIR, refreshAllMetrics, ], ); diff --git a/frontend/src/api/prometheus/usePrometheusQueryRange.ts b/frontend/src/api/prometheus/usePrometheusQueryRange.ts index 71b8875db8..27920e67ff 100644 --- a/frontend/src/api/prometheus/usePrometheusQueryRange.ts +++ b/frontend/src/api/prometheus/usePrometheusQueryRange.ts @@ -1,19 +1,27 @@ import * as React from 'react'; import axios from 'axios'; -import { PrometheusQueryRangeResponse, PrometheusQueryRangeResultValue } from '~/types'; + import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; +import { + PrometheusQueryRangeResponse, + PrometheusQueryRangeResponseData, + PrometheusQueryRangeResultValue, +} from '~/types'; + +export type ResponsePredicate = ( + data: PrometheusQueryRangeResponseData, +) => T[]; -const usePrometheusQueryRange = ( +const usePrometheusQueryRange = ( active: boolean, apiPath: string, queryLang: string, span: number, endInMs: number, step: number, -): FetchState => { - const fetchData = React.useCallback< - FetchStateCallbackPromise - >(() => { + responsePredicate?: ResponsePredicate, +): FetchState => { + const fetchData = React.useCallback>(() => { const endInS = endInMs / 1000; const start = endInS - span; @@ -22,10 +30,18 @@ const usePrometheusQueryRange = ( query: `query=${queryLang}&start=${start}&end=${endInS}&step=${step}`, }) - .then((response) => response.data?.response.data.result?.[0]?.values || []); - }, [queryLang, apiPath, span, endInMs, step]); + .then((response) => { + let result: T[] | PrometheusQueryRangeResultValue[]; + if (responsePredicate) { + result = responsePredicate(response.data?.response.data); + } else { + result = response.data?.response.data.result?.[0]?.values || []; + } + return result as T[]; + }); + }, [endInMs, span, apiPath, queryLang, step, responsePredicate]); - return useFetchState(fetchData, []); + return useFetchState(fetchData, []); }; export default usePrometheusQueryRange; diff --git a/frontend/src/api/prometheus/useQueryRangeResourceData.ts b/frontend/src/api/prometheus/useQueryRangeResourceData.ts index 47d33655c4..b5915a0d0c 100644 --- a/frontend/src/api/prometheus/useQueryRangeResourceData.ts +++ b/frontend/src/api/prometheus/useQueryRangeResourceData.ts @@ -1,8 +1,13 @@ +import * as React from 'react'; import { TimeframeStep, TimeframeTimeRange } from '~/pages/modelServing/screens/const'; import { TimeframeTitle } from '~/pages/modelServing/screens/types'; -import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; +import { + ContextResourceData, + PrometheusQueryRangeResponseDataResult, + PrometheusQueryRangeResultValue, +} from '~/types'; import { useContextResourceData } from '~/utilities/useContextResourceData'; -import usePrometheusQueryRange from './usePrometheusQueryRange'; +import usePrometheusQueryRange, { ResponsePredicate } from './usePrometheusQueryRange'; const useQueryRangeResourceData = ( /** Is the query active -- should we be fetching? */ @@ -23,4 +28,30 @@ const useQueryRangeResourceData = ( 5 * 60 * 1000, ); +type TrustyData = PrometheusQueryRangeResponseDataResult; + +export const useQueryRangeResourceDataTrusty = ( + /** Is the query active -- should we be fetching? */ + active: boolean, + query: string, + end: number, + timeframe: TimeframeTitle, +): ContextResourceData => { + const responsePredicate = React.useCallback>( + (data) => data.result, + [], + ); + return useContextResourceData( + usePrometheusQueryRange( + active, + '/api/prometheus/serving', + query, + TimeframeTimeRange[timeframe], + end, + TimeframeStep[timeframe], + responsePredicate, + ), + 5 * 60 * 1000, + ); +}; export default useQueryRangeResourceData; diff --git a/frontend/src/pages/modelServing/ModelServingRoutes.tsx b/frontend/src/pages/modelServing/ModelServingRoutes.tsx index aa67644dc8..610a595ecd 100644 --- a/frontend/src/pages/modelServing/ModelServingRoutes.tsx +++ b/frontend/src/pages/modelServing/ModelServingRoutes.tsx @@ -13,7 +13,7 @@ const ModelServingRoutes: React.FC = () => { }> } /> : } diff --git a/frontend/src/pages/modelServing/screens/const.ts b/frontend/src/pages/modelServing/screens/const.ts index 2a83ab2b23..f0b803a9b9 100644 --- a/frontend/src/pages/modelServing/screens/const.ts +++ b/frontend/src/pages/modelServing/screens/const.ts @@ -127,6 +127,7 @@ export const DEFAULT_MODEL_SERVING_TEMPLATE: ServingRuntimeKind = { * Unit is in seconds */ export const TimeframeTimeRange: TimeframeTimeType = { + [TimeframeTitle.FIFTEEN_MINUTES]: 15 * 60, [TimeframeTitle.ONE_HOUR]: 60 * 60, [TimeframeTitle.ONE_DAY]: 24 * 60 * 60, [TimeframeTitle.ONE_WEEK]: 7 * 24 * 60 * 60, @@ -143,6 +144,7 @@ export const TimeframeTimeRange: TimeframeTimeType = { * 86,400 / (24 * 12) => 300 points of prometheus data */ export const TimeframeStep: TimeframeStepType = { + [TimeframeTitle.FIFTEEN_MINUTES]: 3, [TimeframeTitle.ONE_HOUR]: 12, [TimeframeTitle.ONE_DAY]: 24 * 12, [TimeframeTitle.ONE_WEEK]: 7 * 24 * 12, diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx new file mode 100644 index 0000000000..b7a335c902 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { PageSection, Stack, StackItem } from '@patternfly/react-core'; +import DIRGraph from '~/pages/modelServing/screens/metrics/DIRChart'; +import MetricsPageToolbar from './MetricsPageToolbar'; +import SPDChart from './SPDChart'; + +const BiasTab = () => ( + + + + + + + + + + + + + + + +); + +export default BiasTab; diff --git a/frontend/src/pages/modelServing/screens/metrics/DIRChart.tsx b/frontend/src/pages/modelServing/screens/metrics/DIRChart.tsx new file mode 100644 index 0000000000..5d3c9f056e --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/DIRChart.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Stack, StackItem } from '@patternfly/react-core'; +import { InferenceMetricType } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import TrustyChart from '~/pages/modelServing/screens/metrics/TrustyChart'; +import { DomainCalculator, MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; + +const DIRChart = () => { + const DEFAULT_MAX_THRESHOLD = 1.2; + const DEFAULT_MIN_THRESHOLD = 0.8; + + const domainCalc: DomainCalculator = (maxYValue) => ({ + y: maxYValue > 1.2 ? [0, maxYValue + 0.1] : [0, 1.3], + }); + + return ( + + + Disparate Impact Ratio (DIR) measures imbalances in classifications by calculating the + ratio between the proportion of the majority and protected classes getting a particular + outcome. + + + Typically, the further away the DIR is from 1, the more unfair the model. A DIR equal to + 1 indicates a perfectly fair model for the groups and outcomes in question. + + + } + domain={domainCalc} + thresholds={[DEFAULT_MAX_THRESHOLD, DEFAULT_MIN_THRESHOLD]} + type={MetricsChartTypes.LINE} + /> + ); +}; + +export default DIRChart; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx index 6cb64d1efc..525c0d9b49 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx @@ -1,18 +1,23 @@ import * as React from 'react'; import { Card, + CardActions, CardBody, + CardHeader, CardTitle, EmptyState, EmptyStateIcon, Spinner, Title, + Toolbar, + ToolbarContent, } from '@patternfly/react-core'; import { Chart, ChartArea, ChartAxis, ChartGroup, + ChartLine, ChartThemeColor, ChartThreshold, ChartVoronoiContainer, @@ -21,46 +26,67 @@ import { import { CubesIcon } from '@patternfly/react-icons'; import { TimeframeTimeRange } from '~/pages/modelServing/screens/const'; import { ModelServingMetricsContext } from './ModelServingMetricsContext'; -import { MetricChartLine, ProcessedMetrics } from './types'; +import { + DomainCalculator, + MetricChartLine, + MetricChartThreshold, + MetricsChartTypes, + ProcessedMetrics, +} from './types'; import { convertTimestamp, + createGraphMetricLine, formatToShow, getThresholdData, - createGraphMetricLine, useStableMetrics, } from './utils'; +const defaultDomainCalculator: DomainCalculator = (maxYValue) => ({ + y: maxYValue === 0 ? [0, 1] : [0, maxYValue], +}); + type MetricsChartProps = { title: string; color?: string; metrics: MetricChartLine; - threshold?: number; + thresholds?: MetricChartThreshold[]; + domain?: DomainCalculator; + toolbar?: React.ReactElement; + type?: MetricsChartTypes; }; - const MetricsChart: React.FC = ({ title, color, metrics: unstableMetrics, - threshold, + thresholds = [], + domain = defaultDomainCalculator, + toolbar, + type = MetricsChartTypes.AREA, }) => { const bodyRef = React.useRef(null); const [chartWidth, setChartWidth] = React.useState(0); const { currentTimeframe, lastUpdateTime } = React.useContext(ModelServingMetricsContext); const metrics = useStableMetrics(unstableMetrics, title); - const { data: graphLines, maxYValue } = React.useMemo( + const { + data: graphLines, + maxYValue, + minYValue, + } = React.useMemo( () => metrics.reduce( (acc, metric) => { const lineValues = createGraphMetricLine(metric); const newMaxValue = Math.max(...lineValues.map((v) => v.y)); + const newMinValue = Math.min(...lineValues.map((v) => v.y)); return { data: [...acc.data, lineValues], maxYValue: Math.max(acc.maxYValue, newMaxValue), + minYValue: Math.min(acc.minYValue, newMinValue), }; }, - { data: [], maxYValue: 0 }, + { data: [], maxYValue: 0, minYValue: 0 }, ), [metrics], ); @@ -94,7 +120,14 @@ const MetricsChart: React.FC = ({ return ( - {title} + + {title} + {toolbar && ( + + {toolbar} + + )} +
{hasSomeData ? ( @@ -106,7 +139,7 @@ const MetricsChart: React.FC = ({ constrainToVisibleArea /> } - domain={{ y: maxYValue === 0 ? [0, 1] : [0, maxYValue + 1] }} + domain={domain(maxYValue, minYValue)} height={400} width={chartWidth} padding={{ left: 70, right: 50, bottom: 70, top: 50 }} @@ -123,11 +156,25 @@ const MetricsChart: React.FC = ({ /> - {graphLines.map((line, i) => ( - - ))} + {graphLines.map((line, i) => { + switch (type) { + case MetricsChartTypes.AREA: + return ; + break; + case MetricsChartTypes.LINE: + return ; + break; + } + })} - {threshold && } + {thresholds.map((t, i) => ( + + ))} ) : ( diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx index 4372349788..ac752c44d2 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; -import { Breadcrumb, BreadcrumbItem, PageSection } from '@patternfly/react-core'; + +import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; import { Link } from 'react-router-dom'; import { BreadcrumbItemType } from '~/types'; import ApplicationsPage from '~/pages/ApplicationsPage'; -import MetricsPageToolbar from './MetricsPageToolbar'; +import MetricsPageTabs from '~/pages/modelServing/screens/metrics/MetricsPageTabs'; type MetricsPageProps = { children: React.ReactNode; @@ -11,7 +12,7 @@ type MetricsPageProps = { breadcrumbItems: BreadcrumbItemType[]; }; -const MetricsPage: React.FC = ({ children, title, breadcrumbItems }) => ( +const MetricsPage: React.FC = ({ title, breadcrumbItems }) => ( = ({ children, title, breadcrumbIt ))} } - toolbar={} loaded description={null} empty={false} > - {children} + ); diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.scss b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.scss new file mode 100644 index 0000000000..f627a91f8a --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.scss @@ -0,0 +1,8 @@ +// This is a hack to get around a bug in PatternFly TabContent. +.odh-tabcontent-fix { + flex-grow: 1; +} +// This is another hack to get around a bug in PatternFly Tabs component. +.odh-tabs-fix { + flex-shrink: 0; +} diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx new file mode 100644 index 0000000000..3e57b5eb55 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Tabs, Tab, TabTitleText } from '@patternfly/react-core'; +import PerformanceTab from './PerformanceTab'; +import BiasTab from './BiasTab'; +import './MetricsPageTabs.scss'; + +const MetricsPageTabs: React.FC = () => { + const DEFAULT_TAB = 'performance'; + + const { tab } = useParams(); + const navigate = useNavigate(); + + React.useEffect(() => { + if (!tab) { + navigate(`./${DEFAULT_TAB}`, { replace: true }); + } + }, [navigate, tab]); + + const loadTab = (tabId: string) => { + navigate(`../${tabId}`, { relative: 'path' }); + }; + + const handleTabClick = ( + event: React.MouseEvent | React.KeyboardEvent | MouseEvent, + tabId: string | number, + ) => { + loadTab(String(tabId)); + }; + + return ( + + Performance} + aria-label="Performance tab" + className="odh-tabcontent-fix" + > + + + Bias} + aria-label="Bias tab" + className="odh-tabcontent-fix" + > + + + + ); +}; + +export default MetricsPageTabs; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx index 9ece1bc5e3..53e1b5010e 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx @@ -20,7 +20,7 @@ const MetricsPageToolbar: React.FC = () => { ModelServingMetricsContext, ); return ( - + + {options.map((value) => ( + + ))} + + ); +}; +export default ScheduledMetricSelect; diff --git a/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx b/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx new file mode 100644 index 0000000000..0d18893169 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Stack, ToolbarContent, ToolbarItem, Tooltip } from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import ScheduledMetricSelect from '~/pages/modelServing/screens/metrics/ScheduledMetricSelect'; +import { + InferenceMetricType, + ModelServingMetricsContext, +} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import { DomainCalculator, MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; + +type TrustyChartProps = { + title: string; + abbreviation: string; + metricType: InferenceMetricType.TRUSTY_AI_SPD | InferenceMetricType.TRUSTY_AI_DIR; + tooltip?: React.ReactElement; + thresholds: [number, number]; + domain: DomainCalculator; + type?: MetricsChartTypes; +}; + +const TrustyChart: React.FC = ({ + title, + abbreviation, + metricType, + tooltip, + thresholds, + domain, + type = MetricsChartTypes.AREA, +}) => { + const THRESHOLD_COLOR = 'red'; + const { data } = React.useContext(ModelServingMetricsContext); + const [selectedPayloadName, setSelectedPayloadName] = React.useState(); + + const metricData = data[metricType].data; + + //TODO: Fix this. This is a short term hack to add a property that will be provided by TrustyAI by release time. + metricData.forEach((x, i) => { + if (!x.metric?.registeredName) { + x.metric.registeredName = `Payload ${i}`; + } + }); + + React.useEffect(() => { + if (!selectedPayloadName) { + setSelectedPayloadName(metricData[0]?.metric?.registeredName); + } + }, [selectedPayloadName, metricData]); + + const payloadOptions: string[] = metricData.map((payload) => payload.metric.registeredName); + + const selectedPayload = metricData.find((x) => x.metric.registeredName === selectedPayloadName); + + const metric = { + ...data[metricType], + data: selectedPayload?.values, + }; + + return ( + + {tooltip && ( + +
+ + + +
+
+ )} + Scheduled Metric + + + +
+ } + thresholds={thresholds.map((t) => ({ + value: t, + color: THRESHOLD_COLOR, + }))} + type={type} + /> + ); +}; +export default TrustyChart; diff --git a/frontend/src/pages/modelServing/screens/metrics/types.ts b/frontend/src/pages/modelServing/screens/metrics/types.ts index bb225f684b..ce6822e5c8 100644 --- a/frontend/src/pages/modelServing/screens/metrics/types.ts +++ b/frontend/src/pages/modelServing/screens/metrics/types.ts @@ -1,3 +1,4 @@ +import { DomainTuple, ForAxes } from 'victory-core'; import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; export type TranslatePoint = (line: GraphMetricPoint) => GraphMetricPoint; @@ -25,4 +26,19 @@ export type GraphMetricLine = GraphMetricPoint[]; export type ProcessedMetrics = { data: GraphMetricLine[]; maxYValue: number; + minYValue: number; }; + +//TODO: color should be an enum of limited PF values and red, not an openended string. +export type MetricChartThreshold = { + value: number; + color?: string; + label?: string; +}; + +export type DomainCalculator = (maxYValue: number, minYValue: number) => ForAxes; + +export enum MetricsChartTypes { + AREA, + LINE, +} diff --git a/frontend/src/pages/modelServing/screens/metrics/utils.ts b/frontend/src/pages/modelServing/screens/metrics/utils.ts index b22c9c2e16..a6c2d2984a 100644 --- a/frontend/src/pages/modelServing/screens/metrics/utils.ts +++ b/frontend/src/pages/modelServing/screens/metrics/utils.ts @@ -41,10 +41,12 @@ export const getInferenceServiceMetricsQueries = ( ): Record => { const namespace = inferenceService.metadata.namespace; const name = inferenceService.metadata.name; + return { - // TODO: Fix queries [InferenceMetricType.REQUEST_COUNT_SUCCESS]: `sum(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}"})`, [InferenceMetricType.REQUEST_COUNT_FAILED]: `sum(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}"})`, + [InferenceMetricType.TRUSTY_AI_SPD]: `trustyai_spd{model="${name}"}`, + [InferenceMetricType.TRUSTY_AI_DIR]: `trustyai_dir{model="${name}"}`, }; }; @@ -119,7 +121,7 @@ export const createGraphMetricLine = ({ metric.data?.map((data) => { const point: GraphMetricPoint = { x: data[0] * 1000, - y: parseInt(data[1]), + y: parseFloat(data[1]), name, }; if (translatePoint) { @@ -144,6 +146,5 @@ export const useStableMetrics = ( ) { metricsRef.current = metrics; } - return metricsRef.current; }; diff --git a/frontend/src/pages/modelServing/screens/types.ts b/frontend/src/pages/modelServing/screens/types.ts index f5b0439786..2aa223ba90 100644 --- a/frontend/src/pages/modelServing/screens/types.ts +++ b/frontend/src/pages/modelServing/screens/types.ts @@ -4,9 +4,11 @@ import { ContainerResources } from '~/types'; export enum MetricType { RUNTIME = 'runtime', INFERENCE = 'inference', + TRUST_AI = 'trustyai', } export enum TimeframeTitle { + FIFTEEN_MINUTES = '15 minutes', ONE_HOUR = '1 hour', ONE_DAY = '24 hours', ONE_WEEK = '7 days', diff --git a/frontend/src/pages/projects/ProjectViewRoutes.tsx b/frontend/src/pages/projects/ProjectViewRoutes.tsx index 57ba7c423d..d6ae3c2f29 100644 --- a/frontend/src/pages/projects/ProjectViewRoutes.tsx +++ b/frontend/src/pages/projects/ProjectViewRoutes.tsx @@ -22,7 +22,7 @@ const ProjectViewRoutes: React.FC = () => { {modelMetricsEnabled && ( <> } /> } /> diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a02112b622..7c2e46f90f 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -17,17 +17,17 @@ export type PrometheusQueryResponse = { status: string; }; +export type PrometheusQueryRangeResponseDataResult = { + // not used -- see https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries for more info + metric: unknown; + values: PrometheusQueryRangeResultValue[]; +}; +export type PrometheusQueryRangeResponseData = { + result: PrometheusQueryRangeResponseDataResult[]; + resultType: string; +}; export type PrometheusQueryRangeResponse = { - data: { - result: [ - { - // not used -- see https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries for more info - metric: unknown; - values: PrometheusQueryRangeResultValue[]; - }, - ]; - resultType: string; - }; + data: PrometheusQueryRangeResponseData; status: string; };