From 6943b1f6b69e8980cf1ace1c2be8524758e12bdd Mon Sep 17 00:00:00 2001 From: Andrew Ballantyne Date: Thu, 16 Mar 2023 13:55:43 -0400 Subject: [PATCH 01/17] Re-enable Metrics --- .../modelServing/useModelMetricsEnabled.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/modelServing/useModelMetricsEnabled.ts b/frontend/src/pages/modelServing/useModelMetricsEnabled.ts index 94d31a8878..8319193654 100644 --- a/frontend/src/pages/modelServing/useModelMetricsEnabled.ts +++ b/frontend/src/pages/modelServing/useModelMetricsEnabled.ts @@ -1,16 +1,14 @@ -// import { useAppContext } from '~/app/AppContext'; -// import { useDashboardNamespace } from '~/redux/selectors'; -// import { isModelMetricsEnabled } from './screens/metrics/utils'; -// -// const useModelMetricsEnabled = (): [modelMetricsEnabled: boolean] => { -// const { dashboardNamespace } = useDashboardNamespace(); -// const { dashboardConfig } = useAppContext(); -// -// const checkModelMetricsEnabled = () => isModelMetricsEnabled(dashboardNamespace, dashboardConfig); -// -// return [checkModelMetricsEnabled()]; -// }; +import { useAppContext } from '~/app/AppContext'; +import { useDashboardNamespace } from '~/redux/selectors'; +import { isModelMetricsEnabled } from './screens/metrics/utils'; -const useModelMetricsEnabled = () => [false]; +const useModelMetricsEnabled = (): [modelMetricsEnabled: boolean] => { + const { dashboardNamespace } = useDashboardNamespace(); + const { dashboardConfig } = useAppContext(); + + const checkModelMetricsEnabled = () => isModelMetricsEnabled(dashboardNamespace, dashboardConfig); + + return [checkModelMetricsEnabled()]; +}; export default useModelMetricsEnabled; From d9e950c6a968d53cad800de90e8f038f11e79d59 Mon Sep 17 00:00:00 2001 From: Andrew Ballantyne <8126518+andrewballantyne@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:23:32 -0400 Subject: [PATCH 02/17] Fix Graphs & Configure Infrastructure (#1023) * Rework Inference Metrics & Add Runtime Metrics (invalid quieries) * Add stacked line chart functionality --- frontend/src/api/prometheus/serving.ts | 79 +++++++++++----- .../api/prometheus/usePrometheusQueryRange.ts | 11 ++- .../prometheus/useQueryRangeResourceData.ts | 7 +- .../pages/modelServing/ModelServingRoutes.tsx | 5 +- .../src/pages/modelServing/screens/const.ts | 22 ++++- ....tsx => GlobalInferenceMetricsWrapper.tsx} | 12 ++- .../screens/metrics/InferenceGraphs.tsx | 45 +++++++++ .../screens/metrics/MetricsChart.tsx | 90 ++++++++++++------ .../screens/metrics/MetricsPage.tsx | 93 ++++++------------- .../metrics/ModelServingMetricsContext.tsx | 41 +++++--- .../screens/metrics/RuntimeGraphs.tsx | 51 ++++++++++ .../modelServing/screens/metrics/types.ts | 28 ++++++ .../modelServing/screens/metrics/utils.ts | 92 +++++++++++++++--- ...tsx => ProjectInferenceMetricsWrapper.tsx} | 21 +++-- .../projects/ProjectRuntimeMetricsWrapper.tsx | 53 +++++++++++ .../projects/ServingRuntimeTableRow.tsx | 12 ++- .../src/pages/modelServing/screens/types.ts | 10 +- .../src/pages/projects/ProjectViewRoutes.tsx | 18 ++-- 18 files changed, 504 insertions(+), 186 deletions(-) rename frontend/src/pages/modelServing/screens/metrics/{ModelServingMetricsWrapper.tsx => GlobalInferenceMetricsWrapper.tsx} (79%) create mode 100644 frontend/src/pages/modelServing/screens/metrics/InferenceGraphs.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/RuntimeGraphs.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/types.ts rename frontend/src/pages/modelServing/screens/projects/{DetailsPageMetricsWrapper.tsx => ProjectInferenceMetricsWrapper.tsx} (73%) create mode 100644 frontend/src/pages/modelServing/screens/projects/ProjectRuntimeMetricsWrapper.tsx diff --git a/frontend/src/api/prometheus/serving.ts b/frontend/src/api/prometheus/serving.ts index 2c079469a5..2f0df96cd8 100644 --- a/frontend/src/api/prometheus/serving.ts +++ b/frontend/src/api/prometheus/serving.ts @@ -1,42 +1,62 @@ import * as React from 'react'; import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; -import { ModelServingMetricType } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { + InferenceMetricType, + RuntimeMetricType, +} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import { MetricType, TimeframeTitle } from '~/pages/modelServing/screens/types'; import useQueryRangeResourceData from './useQueryRangeResourceData'; export const useModelServingMetrics = ( - queries: Record, + type: MetricType, + queries: Record | Record, timeframe: TimeframeTitle, lastUpdateTime: number, setLastUpdateTime: (time: number) => void, ): { - data: Record>; + data: Record>; refresh: () => void; } => { const [end, setEnd] = React.useState(lastUpdateTime); - const endpointHealth = useQueryRangeResourceData( - queries[ModelServingMetricType.ENDPOINT_HEALTH], + const runtimeRequestCount = useQueryRangeResourceData( + type === 'runtime', + queries[RuntimeMetricType.REQUEST_COUNT], end, timeframe, ); - const inferencePerformance = useQueryRangeResourceData( - queries[ModelServingMetricType.INFERENCE_PERFORMANCE], + + const runtimeAverageResponseTime = useQueryRangeResourceData( + type === 'runtime', + queries[RuntimeMetricType.AVG_RESPONSE_TIME], + end, + timeframe, + ); + + const runtimeCPUUtilization = useQueryRangeResourceData( + type === 'runtime', + queries[RuntimeMetricType.CPU_UTILIZATION], end, timeframe, ); - const averageResponseTime = useQueryRangeResourceData( - queries[ModelServingMetricType.AVG_RESPONSE_TIME], + + const runtimeMemoryUtilization = useQueryRangeResourceData( + type === 'runtime', + queries[RuntimeMetricType.MEMORY_UTILIZATION], end, timeframe, ); - const requestCount = useQueryRangeResourceData( - queries[ModelServingMetricType.REQUEST_COUNT], + + const inferenceRequestSuccessCount = useQueryRangeResourceData( + type === 'inference', + queries[InferenceMetricType.REQUEST_COUNT_SUCCESS], end, timeframe, ); - const failedRequestCount = useQueryRangeResourceData( - queries[ModelServingMetricType.FAILED_REQUEST_COUNT], + + const inferenceRequestFailedCount = useQueryRangeResourceData( + type === 'inference', + queries[InferenceMetricType.REQUEST_COUNT_FAILED], end, timeframe, ); @@ -45,7 +65,14 @@ export const useModelServingMetrics = ( setLastUpdateTime(Date.now()); // re-compute lastUpdateTime when data changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [endpointHealth, inferencePerformance, averageResponseTime, requestCount, failedRequestCount]); + }, [ + runtimeRequestCount, + runtimeAverageResponseTime, + runtimeCPUUtilization, + runtimeMemoryUtilization, + inferenceRequestSuccessCount, + inferenceRequestFailedCount, + ]); const refreshAllMetrics = React.useCallback(() => { setEnd(Date.now()); @@ -54,20 +81,22 @@ export const useModelServingMetrics = ( return React.useMemo( () => ({ data: { - [ModelServingMetricType.ENDPOINT_HEALTH]: endpointHealth, - [ModelServingMetricType.INFERENCE_PERFORMANCE]: inferencePerformance, - [ModelServingMetricType.AVG_RESPONSE_TIME]: averageResponseTime, - [ModelServingMetricType.REQUEST_COUNT]: requestCount, - [ModelServingMetricType.FAILED_REQUEST_COUNT]: failedRequestCount, + [RuntimeMetricType.REQUEST_COUNT]: runtimeRequestCount, + [RuntimeMetricType.AVG_RESPONSE_TIME]: runtimeAverageResponseTime, + [RuntimeMetricType.CPU_UTILIZATION]: runtimeCPUUtilization, + [RuntimeMetricType.MEMORY_UTILIZATION]: runtimeMemoryUtilization, + [InferenceMetricType.REQUEST_COUNT_SUCCESS]: inferenceRequestSuccessCount, + [InferenceMetricType.REQUEST_COUNT_FAILED]: inferenceRequestFailedCount, }, refresh: refreshAllMetrics, }), [ - endpointHealth, - inferencePerformance, - averageResponseTime, - requestCount, - failedRequestCount, + runtimeRequestCount, + runtimeAverageResponseTime, + runtimeCPUUtilization, + runtimeMemoryUtilization, + inferenceRequestSuccessCount, + inferenceRequestFailedCount, refreshAllMetrics, ], ); diff --git a/frontend/src/api/prometheus/usePrometheusQueryRange.ts b/frontend/src/api/prometheus/usePrometheusQueryRange.ts index 1c5b4efa6e..ec805fd847 100644 --- a/frontend/src/api/prometheus/usePrometheusQueryRange.ts +++ b/frontend/src/api/prometheus/usePrometheusQueryRange.ts @@ -3,6 +3,7 @@ import axios from 'axios'; import { PrometheusQueryRangeResponse, PrometheusQueryRangeResultValue } from '~/types'; const usePrometheusQueryRange = ( + active: boolean, apiPath: string, queryLang: string, span: number, @@ -22,9 +23,15 @@ const usePrometheusQueryRange = ( const endInS = endInMs / 1000; const start = endInS - span; + if (!active) { + // Save us the call & data storage -- if it's not active, we don't need to fetch + // If we are already loaded & have data, it's okay -- it can be stale data to quickly show + // if the associated graph renders + return; + } axios .post<{ response: PrometheusQueryRangeResponse }>(apiPath, { - query: `${queryLang}&start=${start}&end=${endInS}&step=${step}`, + query: `query=${queryLang}&start=${start}&end=${endInS}&step=${step}`, }) .then((response) => { const result = response.data?.response.data.result?.[0]?.values || []; @@ -36,7 +43,7 @@ const usePrometheusQueryRange = ( setError(e); setLoaded(true); }); - }, [queryLang, apiPath, span, endInMs, step]); + }, [endInMs, span, active, apiPath, queryLang, step]); React.useEffect(() => { fetchData(); diff --git a/frontend/src/api/prometheus/useQueryRangeResourceData.ts b/frontend/src/api/prometheus/useQueryRangeResourceData.ts index 2f9ef5bc1e..47d33655c4 100644 --- a/frontend/src/api/prometheus/useQueryRangeResourceData.ts +++ b/frontend/src/api/prometheus/useQueryRangeResourceData.ts @@ -1,19 +1,22 @@ -import { TimeframeStep, TimeframeTime } from '~/pages/modelServing/screens/const'; +import { TimeframeStep, TimeframeTimeRange } from '~/pages/modelServing/screens/const'; import { TimeframeTitle } from '~/pages/modelServing/screens/types'; import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; import { useContextResourceData } from '~/utilities/useContextResourceData'; import usePrometheusQueryRange from './usePrometheusQueryRange'; const useQueryRangeResourceData = ( + /** Is the query active -- should we be fetching? */ + active: boolean, query: string, end: number, timeframe: TimeframeTitle, ): ContextResourceData => useContextResourceData( usePrometheusQueryRange( + active, '/api/prometheus/serving', query, - TimeframeTime[timeframe], + TimeframeTimeRange[timeframe], end, TimeframeStep[timeframe], ), diff --git a/frontend/src/pages/modelServing/ModelServingRoutes.tsx b/frontend/src/pages/modelServing/ModelServingRoutes.tsx index a01dc90b78..aa67644dc8 100644 --- a/frontend/src/pages/modelServing/ModelServingRoutes.tsx +++ b/frontend/src/pages/modelServing/ModelServingRoutes.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Navigate, Routes, Route } from 'react-router-dom'; import ModelServingContextProvider from './ModelServingContext'; -import ModelServingMetricsWrapper from './screens/metrics/ModelServingMetricsWrapper'; +import GlobalInferenceMetricsWrapper from './screens/metrics/GlobalInferenceMetricsWrapper'; import ModelServingGlobal from './screens/global/ModelServingGlobal'; import useModelMetricsEnabled from './useModelMetricsEnabled'; @@ -15,9 +15,10 @@ const ModelServingRoutes: React.FC = () => { : + modelMetricsEnabled ? : } /> + {/* TODO: Global Runtime metrics?? */} } /> diff --git a/frontend/src/pages/modelServing/screens/const.ts b/frontend/src/pages/modelServing/screens/const.ts index 51853b7dca..2a83ab2b23 100644 --- a/frontend/src/pages/modelServing/screens/const.ts +++ b/frontend/src/pages/modelServing/screens/const.ts @@ -122,18 +122,30 @@ export const DEFAULT_MODEL_SERVING_TEMPLATE: ServingRuntimeKind = { }, }; -// unit: seconds -export const TimeframeTime: TimeframeTimeType = { - [TimeframeTitle.FIVE_MINUTES]: 5 * 60, +/** + * The desired range (x-axis) of the charts. + * Unit is in seconds + */ +export const TimeframeTimeRange: TimeframeTimeType = { [TimeframeTitle.ONE_HOUR]: 60 * 60, [TimeframeTitle.ONE_DAY]: 24 * 60 * 60, [TimeframeTitle.ONE_WEEK]: 7 * 24 * 60 * 60, + [TimeframeTitle.ONE_MONTH]: 30 * 7 * 24 * 60 * 60, + // [TimeframeTitle.UNLIMITED]: 0, }; -// make sure we always get ~300 data points +/** + * How large a step is -- value is in how many seconds to combine to great an individual data response + * Each should be getting ~300 data points (assuming data fills the gap) + * + * eg. [TimeframeTitle.ONE_DAY]: 24 * 12, + * 24h * 60m * 60s => 86,400 seconds of space + * 86,400 / (24 * 12) => 300 points of prometheus data + */ export const TimeframeStep: TimeframeStepType = { - [TimeframeTitle.FIVE_MINUTES]: 1, [TimeframeTitle.ONE_HOUR]: 12, [TimeframeTitle.ONE_DAY]: 24 * 12, [TimeframeTitle.ONE_WEEK]: 7 * 24 * 12, + [TimeframeTitle.ONE_MONTH]: 30 * 7 * 24 * 12, + // [TimeframeTitle.UNLIMITED]: 30 * 7 * 24 * 12, // TODO: determine if we "zoom out" more }; diff --git a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx similarity index 79% rename from frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsWrapper.tsx rename to frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx index 013ebb7476..987b13dec4 100644 --- a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsWrapper.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx @@ -4,11 +4,13 @@ import { Bullseye, Spinner } from '@patternfly/react-core'; import NotFound from '~/pages/NotFound'; import { ModelServingContext } from '~/pages/modelServing/ModelServingContext'; import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; +import InferenceGraphs from '~/pages/modelServing/screens/metrics/InferenceGraphs'; +import { MetricType } from '~/pages/modelServing/screens/types'; import { ModelServingMetricsProvider } from './ModelServingMetricsContext'; import MetricsPage from './MetricsPage'; import { getInferenceServiceMetricsQueries } from './utils'; -const ModelServingMetricsWrapper: React.FC = () => { +const GlobalInferenceMetricsWrapper: React.FC = () => { const { project: projectName, inferenceService: modelName } = useParams<{ project: string; inferenceService: string; @@ -33,7 +35,7 @@ const ModelServingMetricsWrapper: React.FC = () => { const modelDisplayName = getInferenceServiceDisplayName(inferenceService); return ( - + { isActive: true, }, ]} - /> + > + + ); }; -export default ModelServingMetricsWrapper; +export default GlobalInferenceMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/metrics/InferenceGraphs.tsx b/frontend/src/pages/modelServing/screens/metrics/InferenceGraphs.tsx new file mode 100644 index 0000000000..920756b1c5 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/InferenceGraphs.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { Stack, StackItem } from '@patternfly/react-core'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { + InferenceMetricType, + ModelServingMetricsContext, +} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import { TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { per100 } from './utils'; + +const InferenceGraphs: React.FC = () => { + const { data, currentTimeframe } = React.useContext(ModelServingMetricsContext); + + const inHours = + currentTimeframe === TimeframeTitle.ONE_HOUR || currentTimeframe === TimeframeTitle.ONE_DAY; + + return ( + + + { + // TODO: remove when real values are used + const newPoint = per100(point); + const y = Math.floor(newPoint.y / (Math.floor(Math.random() * 2) + 2)); + return { ...newPoint, y }; + }, + }, + ]} + title={`Http requests per ${inHours ? 'hour' : 'day'} (x100)`} + /> + + + ); +}; + +export default InferenceGraphs; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx index 6a4c5bbf1b..6cb64d1efc 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx @@ -13,42 +13,61 @@ import { ChartArea, ChartAxis, ChartGroup, + ChartThemeColor, ChartThreshold, ChartVoronoiContainer, getResizeObserver, } from '@patternfly/react-charts'; import { CubesIcon } from '@patternfly/react-icons'; -import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; -import { TimeframeTime } from '~/pages/modelServing/screens/const'; +import { TimeframeTimeRange } from '~/pages/modelServing/screens/const'; import { ModelServingMetricsContext } from './ModelServingMetricsContext'; -import { convertTimestamp, formatToShow, getThresholdData } from './utils'; +import { MetricChartLine, ProcessedMetrics } from './types'; +import { + convertTimestamp, + formatToShow, + getThresholdData, + createGraphMetricLine, + useStableMetrics, +} from './utils'; type MetricsChartProps = { title: string; - color: string; - metrics: ContextResourceData; - unit?: string; + color?: string; + metrics: MetricChartLine; threshold?: number; }; -const MetricsChart: React.FC = ({ title, color, metrics, unit, threshold }) => { +const MetricsChart: React.FC = ({ + title, + color, + metrics: unstableMetrics, + threshold, +}) => { const bodyRef = React.useRef(null); const [chartWidth, setChartWidth] = React.useState(0); const { currentTimeframe, lastUpdateTime } = React.useContext(ModelServingMetricsContext); + const metrics = useStableMetrics(unstableMetrics, title); - const processedData = React.useMemo( + const { data: graphLines, maxYValue } = React.useMemo( () => - metrics.data?.map((data) => ({ - x: data[0] * 1000, - y: parseInt(data[1]), - name: title, - })) || [], - [metrics, title], - ); + metrics.reduce( + (acc, metric) => { + const lineValues = createGraphMetricLine(metric); + const newMaxValue = Math.max(...lineValues.map((v) => v.y)); - const maxValue = Math.max(...processedData.map((e) => e.y)); + return { + data: [...acc.data, lineValues], + maxYValue: Math.max(acc.maxYValue, newMaxValue), + }; + }, + { data: [], maxYValue: 0 }, + ), + [metrics], + ); - const hasData = processedData.length > 0; + const error = metrics.find((line) => line.metric.error)?.metric.error; + const isAllLoaded = metrics.every((line) => line.metric.loaded); + const hasSomeData = graphLines.some((line) => line.length > 0); React.useEffect(() => { const ref = bodyRef.current; @@ -61,14 +80,24 @@ const MetricsChart: React.FC = ({ title, color, metrics, unit handleResize(); } return () => observer(); - }, [bodyRef]); + }, []); + + let legendProps: Partial> = {}; + if (metrics.length > 1 && metrics.every(({ name }) => !!name)) { + // We don't need a label if there is only one line & we need a name for every item (or it won't align) + legendProps = { + legendData: metrics.map(({ name }) => ({ name })), + legendOrientation: 'horizontal', + legendPosition: 'bottom-left', + }; + } return ( - {`${title}${unit ? ` (${unit})` : ''}`} - + {title} +
- {hasData ? ( + {hasSomeData ? ( = ({ title, color, metrics, unit constrainToVisibleArea /> } - domain={{ y: maxValue === 0 ? [-1, 1] : [0, maxValue + 1] }} + domain={{ y: maxYValue === 0 ? [0, 1] : [0, maxYValue + 1] }} height={400} width={chartWidth} padding={{ left: 70, right: 50, bottom: 70, top: 50 }} - themeColor={color} + themeColor={color ?? ChartThemeColor.multi} + {...legendProps} > convertTimestamp(x, formatToShow(currentTimeframe))} + tickValues={[]} domain={{ - x: [lastUpdateTime - TimeframeTime[currentTimeframe] * 1000, lastUpdateTime], + x: [lastUpdateTime - TimeframeTimeRange[currentTimeframe] * 1000, lastUpdateTime], }} fixLabelOverlap /> - + {graphLines.map((line, i) => ( + + ))} - {threshold && } + {threshold && } ) : ( - {metrics.loaded ? ( + {isAllLoaded ? ( <> - {metrics.error ? metrics.error.message : 'No available data'} + {error ? error.message : 'No available data'} ) : ( diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx index 701537033d..5b9056c17e 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx @@ -5,78 +5,37 @@ import { BreadcrumbItemType } from '~/types'; import ApplicationsPage from '~/pages/ApplicationsPage'; import MetricsChart from './MetricsChart'; import MetricsPageToolbar from './MetricsPageToolbar'; -import { ModelServingMetricsContext, ModelServingMetricType } from './ModelServingMetricsContext'; +import { ModelServingMetricsContext, RuntimeMetricType } from './ModelServingMetricsContext'; type MetricsPageProps = { + children: React.ReactNode; title: string; breadcrumbItems: BreadcrumbItemType[]; }; -const MetricsPage: React.FC = ({ title, breadcrumbItems }) => { - const { data } = React.useContext(ModelServingMetricsContext); - return ( - - {breadcrumbItems.map((item) => ( - - item.link ? {item.label} : <>{item.label} - } - /> - ))} - - } - toolbar={} - loaded - description={null} - empty={false} - > - - - - - - - - - - - - - - - - - - - - - ); -}; +const MetricsPage: React.FC = ({ children, title, breadcrumbItems }) => ( + + {breadcrumbItems.map((item) => ( + + item.link ? {item.label} : <>{item.label} + } + /> + ))} + + } + toolbar={} + loaded + description={null} + empty={false} + > + {children} + +); export default MetricsPage; diff --git a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx b/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx index 68ab5ac7dc..af3a425377 100644 --- a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx @@ -2,18 +2,25 @@ import * as React from 'react'; import { useModelServingMetrics } from '~/api'; import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; import { DEFAULT_CONTEXT_DATA } from '~/utilities/const'; -import { TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { MetricType, TimeframeTitle } from '~/pages/modelServing/screens/types'; -export enum ModelServingMetricType { - ENDPOINT_HEALTH = 'end-point-health', - INFERENCE_PERFORMANCE = 'inference-performance', - AVG_RESPONSE_TIME = 'avg-response-time', - REQUEST_COUNT = 'request-count', - FAILED_REQUEST_COUNT = 'failed-request-count', +export enum RuntimeMetricType { + AVG_RESPONSE_TIME = 'runtime_avg-response-time', + REQUEST_COUNT = 'runtime_requests-count', + CPU_UTILIZATION = 'runtime_cpu-utilization', + MEMORY_UTILIZATION = 'runtime_memory-utilization', +} + +export enum InferenceMetricType { + REQUEST_COUNT_SUCCESS = 'inference_request-count-successes', + REQUEST_COUNT_FAILED = 'inference_request-count-fails', } type ModelServingMetricsContext = { - data: Record>; + data: Record< + RuntimeMetricType & InferenceMetricType, + ContextResourceData + >; currentTimeframe: TimeframeTitle; setCurrentTimeframe: (timeframe: TimeframeTitle) => void; refresh: () => void; @@ -23,13 +30,14 @@ type ModelServingMetricsContext = { export const ModelServingMetricsContext = React.createContext({ data: { - [ModelServingMetricType.ENDPOINT_HEALTH]: DEFAULT_CONTEXT_DATA, - [ModelServingMetricType.INFERENCE_PERFORMANCE]: DEFAULT_CONTEXT_DATA, - [ModelServingMetricType.AVG_RESPONSE_TIME]: DEFAULT_CONTEXT_DATA, - [ModelServingMetricType.REQUEST_COUNT]: DEFAULT_CONTEXT_DATA, - [ModelServingMetricType.FAILED_REQUEST_COUNT]: DEFAULT_CONTEXT_DATA, + [RuntimeMetricType.REQUEST_COUNT]: DEFAULT_CONTEXT_DATA, + [RuntimeMetricType.AVG_RESPONSE_TIME]: DEFAULT_CONTEXT_DATA, + [RuntimeMetricType.CPU_UTILIZATION]: DEFAULT_CONTEXT_DATA, + [RuntimeMetricType.MEMORY_UTILIZATION]: DEFAULT_CONTEXT_DATA, + [InferenceMetricType.REQUEST_COUNT_FAILED]: DEFAULT_CONTEXT_DATA, + [InferenceMetricType.REQUEST_COUNT_SUCCESS]: DEFAULT_CONTEXT_DATA, }, - currentTimeframe: TimeframeTitle.FIVE_MINUTES, + currentTimeframe: TimeframeTitle.ONE_HOUR, setCurrentTimeframe: () => undefined, refresh: () => undefined, lastUpdateTime: 0, @@ -39,12 +47,14 @@ export const ModelServingMetricsContext = React.createContext; + queries: Record | Record; + type: MetricType; }; export const ModelServingMetricsProvider: React.FC = ({ queries, children, + type, }) => { const [currentTimeframe, setCurrentTimeframe] = React.useState( TimeframeTitle.ONE_DAY, @@ -52,6 +62,7 @@ export const ModelServingMetricsProvider: React.FC(Date.now()); const { data, refresh } = useModelServingMetrics( + type, queries, currentTimeframe, lastUpdateTime, diff --git a/frontend/src/pages/modelServing/screens/metrics/RuntimeGraphs.tsx b/frontend/src/pages/modelServing/screens/metrics/RuntimeGraphs.tsx new file mode 100644 index 0000000000..e79f4d9af8 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/RuntimeGraphs.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { Stack, StackItem } from '@patternfly/react-core'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { + ModelServingMetricsContext, + RuntimeMetricType, +} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import { TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { per100 } from '~/pages/modelServing/screens/metrics/utils'; + +const RuntimeGraphs: React.FC = () => { + const { data, currentTimeframe } = React.useContext(ModelServingMetricsContext); + + const inHours = + currentTimeframe === TimeframeTitle.ONE_HOUR || currentTimeframe === TimeframeTitle.ONE_DAY; + + return ( + + + + + + + + + + + + + + + ); +}; + +export default RuntimeGraphs; diff --git a/frontend/src/pages/modelServing/screens/metrics/types.ts b/frontend/src/pages/modelServing/screens/metrics/types.ts new file mode 100644 index 0000000000..bb225f684b --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/types.ts @@ -0,0 +1,28 @@ +import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; + +export type TranslatePoint = (line: GraphMetricPoint) => GraphMetricPoint; + +type MetricChartLineBase = { + metric: ContextResourceData; + translatePoint?: TranslatePoint; +}; +export type NamedMetricChartLine = MetricChartLineBase & { + name: string; +}; +export type UnnamedMetricChartLine = MetricChartLineBase & { + /** Assumes chart title */ + name?: string; +}; +export type MetricChartLine = UnnamedMetricChartLine | NamedMetricChartLine[]; + +export type GraphMetricPoint = { + x: number; + y: number; + name: string; +}; +export type GraphMetricLine = GraphMetricPoint[]; + +export type ProcessedMetrics = { + data: GraphMetricLine[]; + maxYValue: number; +}; diff --git a/frontend/src/pages/modelServing/screens/metrics/utils.ts b/frontend/src/pages/modelServing/screens/metrics/utils.ts index 7868b3c1aa..b22c9c2e16 100644 --- a/frontend/src/pages/modelServing/screens/metrics/utils.ts +++ b/frontend/src/pages/modelServing/screens/metrics/utils.ts @@ -1,9 +1,17 @@ import * as _ from 'lodash'; import { SelectOptionObject } from '@patternfly/react-core'; +import * as React from 'react'; import { TimeframeTitle } from '~/pages/modelServing/screens/types'; -import { InferenceServiceKind } from '~/k8sTypes'; +import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; import { DashboardConfig } from '~/types'; -import { ModelServingMetricType } from './ModelServingMetricsContext'; +import { + GraphMetricLine, + GraphMetricPoint, + MetricChartLine, + NamedMetricChartLine, + TranslatePoint, +} from '~/pages/modelServing/screens/metrics/types'; +import { InferenceMetricType, RuntimeMetricType } from './ModelServingMetricsContext'; export const isModelMetricsEnabled = ( dashboardNamespace: string, @@ -15,17 +23,28 @@ export const isModelMetricsEnabled = ( return dashboardConfig.spec.dashboardConfig.modelMetricsNamespace !== ''; }; +export const getRuntimeMetricsQueries = ( + runtime: ServingRuntimeKind, +): Record => { + const namespace = runtime.metadata.namespace; + return { + // TODO: Get new queries + [RuntimeMetricType.REQUEST_COUNT]: `TBD`, + [RuntimeMetricType.AVG_RESPONSE_TIME]: `rate(modelmesh_api_request_milliseconds_sum{exported_namespace="${namespace}"}[1m])/rate(modelmesh_api_request_milliseconds_count{exported_namespace="${namespace}"}[1m])`, + [RuntimeMetricType.CPU_UTILIZATION]: `TBD`, + [RuntimeMetricType.MEMORY_UTILIZATION]: `TBD`, + }; +}; + export const getInferenceServiceMetricsQueries = ( inferenceService: InferenceServiceKind, -): Record => { +): Record => { const namespace = inferenceService.metadata.namespace; const name = inferenceService.metadata.name; return { - [ModelServingMetricType.AVG_RESPONSE_TIME]: `query=sum(haproxy_backend_http_average_response_latency_milliseconds{exported_namespace="${namespace}", route="${name}"})`, - [ModelServingMetricType.ENDPOINT_HEALTH]: `query=sum(rate(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}", code=~"5xx"}[5m])) > 0`, - [ModelServingMetricType.FAILED_REQUEST_COUNT]: `query=sum(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}", code=~"4xx|5xx"})`, - [ModelServingMetricType.INFERENCE_PERFORMANCE]: `query=sum(rate(modelmesh_api_request_milliseconds_sum{namespace="${namespace}"}[5m]))`, - [ModelServingMetricType.REQUEST_COUNT]: `query=sum(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}"})`, + // 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}"})`, }; }; @@ -65,19 +84,20 @@ export const convertTimestamp = (timestamp: number, show?: 'date' | 'second'): s } ${ampm}`; }; -export const getThresholdData = ( - data: { x: number; y: number; name: string }[], - threshold: number, -): { x: number; y: number }[] => +export const getThresholdData = (data: GraphMetricLine[], threshold: number): GraphMetricLine => _.uniqBy( - data.map((data) => ({ name: 'Threshold', x: data.x, y: threshold })), + _.uniq( + data.reduce((xValues, line) => [...xValues, ...line.map((point) => point.x)], []), + ).map((xValue) => ({ + name: 'Threshold', + x: xValue, + y: threshold, + })), (value) => value.x, ); export const formatToShow = (timeframe: TimeframeTitle): 'date' | 'second' | undefined => { switch (timeframe) { - case TimeframeTitle.FIVE_MINUTES: - return 'second'; case TimeframeTitle.ONE_HOUR: case TimeframeTitle.ONE_DAY: return undefined; @@ -85,3 +105,45 @@ export const formatToShow = (timeframe: TimeframeTitle): 'date' | 'second' | und return 'date'; } }; + +export const per100: TranslatePoint = (point) => ({ + ...point, + y: point.y / 100, +}); + +export const createGraphMetricLine = ({ + metric, + name, + translatePoint, +}: NamedMetricChartLine): GraphMetricLine => + metric.data?.map((data) => { + const point: GraphMetricPoint = { + x: data[0] * 1000, + y: parseInt(data[1]), + name, + }; + if (translatePoint) { + return translatePoint(point); + } + return point; + }) || []; + +export const useStableMetrics = ( + metricChartLine: MetricChartLine, + chartTitle: string, +): NamedMetricChartLine[] => { + const metricsRef = React.useRef([]); + + const metrics = Array.isArray(metricChartLine) + ? metricChartLine + : [{ ...metricChartLine, name: metricChartLine.name ?? chartTitle }]; + + if ( + metrics.length !== metricsRef.current.length || + metrics.some((graphLine, i) => graphLine.metric !== metricsRef.current[i].metric) + ) { + metricsRef.current = metrics; + } + + return metricsRef.current; +}; diff --git a/frontend/src/pages/modelServing/screens/projects/DetailsPageMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsWrapper.tsx similarity index 73% rename from frontend/src/pages/modelServing/screens/projects/DetailsPageMetricsWrapper.tsx rename to frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsWrapper.tsx index 74b4361916..6cf7a9d150 100644 --- a/frontend/src/pages/modelServing/screens/projects/DetailsPageMetricsWrapper.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsWrapper.tsx @@ -8,19 +8,18 @@ import { getInferenceServiceMetricsQueries } from '~/pages/modelServing/screens/ import NotFound from '~/pages/NotFound'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; import { getProjectDisplayName } from '~/pages/projects/utils'; +import InferenceGraphs from '~/pages/modelServing/screens/metrics/InferenceGraphs'; +import { MetricType } from '~/pages/modelServing/screens/types'; -const DetailsPageMetricsWrapper: React.FC = () => { - const { namespace: projectName, inferenceService: modelName } = useParams<{ - namespace: string; +const ProjectInferenceMetricsWrapper: React.FC = () => { + const { inferenceService: modelName } = useParams<{ inferenceService: string; }>(); const { currentProject, inferenceServices: { data: models, loaded }, } = React.useContext(ProjectDetailsContext); - const inferenceService = models.find( - (model) => model.metadata.name === modelName && model.metadata.namespace === projectName, - ); + const inferenceService = models.find((model) => model.metadata.name === modelName); if (!loaded) { return ( @@ -36,9 +35,9 @@ const DetailsPageMetricsWrapper: React.FC = () => { const modelDisplayName = getInferenceServiceDisplayName(inferenceService); return ( - + { isActive: true, }, ]} - /> + > + + ); }; -export default DetailsPageMetricsWrapper; +export default ProjectInferenceMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectRuntimeMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectRuntimeMetricsWrapper.tsx new file mode 100644 index 0000000000..6e5a1f6b25 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/projects/ProjectRuntimeMetricsWrapper.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import MetricsPage from '~/pages/modelServing/screens/metrics/MetricsPage'; +import { ModelServingMetricsProvider } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import { getRuntimeMetricsQueries } from '~/pages/modelServing/screens/metrics/utils'; +import NotFound from '~/pages/NotFound'; +import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; +import { getProjectDisplayName } from '~/pages/projects/utils'; +import RuntimeGraphs from '~/pages/modelServing/screens/metrics/RuntimeGraphs'; +import { MetricType } from '~/pages/modelServing/screens/types'; + +const ProjectInferenceMetricsWrapper: React.FC = () => { + const { + currentProject, + servingRuntimes: { data: runtimes, loaded }, + } = React.useContext(ProjectDetailsContext); + const runtime = runtimes[0]; + if (!loaded) { + return ( + + + + ); + } + if (!runtime) { + return ; + } + const queries = getRuntimeMetricsQueries(runtime); + const projectDisplayName = getProjectDisplayName(currentProject); + + return ( + + + + + + ); +}; + +export default ProjectInferenceMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx index 781dd1b123..d4256555b8 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import { Button, Icon, Skeleton, Tooltip } from '@patternfly/react-core'; +import { Button, DropdownDirection, Icon, Skeleton, Tooltip } from '@patternfly/react-core'; import { ActionsColumn, Tbody, Td, Tr } from '@patternfly/react-table'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { useNavigate } from 'react-router-dom'; import { ServingRuntimeKind } from '~/k8sTypes'; import EmptyTableCellForAlignment from '~/pages/projects/components/EmptyTableCellForAlignment'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; @@ -27,8 +28,11 @@ const ServingRuntimeTableRow: React.FC = ({ expandedColumn, onExpandColumn, }) => { + const navigate = useNavigate(); + const isRowExpanded = !!expandedColumn; const { + currentProject, inferenceServices: { data: inferenceServices, loaded: inferenceServicesLoaded, @@ -122,6 +126,7 @@ const ServingRuntimeTableRow: React.FC = ({ = ({ title: 'Delete model server', onClick: () => onDeleteServingRuntime(obj), }, + { + title: 'View metrics', + onClick: () => + navigate(`/projects/${currentProject.metadata.name}/metrics/runtime`), + }, ]} /> diff --git a/frontend/src/pages/modelServing/screens/types.ts b/frontend/src/pages/modelServing/screens/types.ts index 6f8f6339eb..f5b0439786 100644 --- a/frontend/src/pages/modelServing/screens/types.ts +++ b/frontend/src/pages/modelServing/screens/types.ts @@ -1,11 +1,17 @@ import { EnvVariableDataEntry } from '~/pages/projects/types'; import { ContainerResources } from '~/types'; +export enum MetricType { + RUNTIME = 'runtime', + INFERENCE = 'inference', +} + export enum TimeframeTitle { - FIVE_MINUTES = '5 minutes', ONE_HOUR = '1 hour', ONE_DAY = '24 hours', - ONE_WEEK = '1 week', + ONE_WEEK = '7 days', + ONE_MONTH = '30 days', + // UNLIMITED = 'Unlimited', } export type TimeframeTimeType = { diff --git a/frontend/src/pages/projects/ProjectViewRoutes.tsx b/frontend/src/pages/projects/ProjectViewRoutes.tsx index 1073186bc4..57ba7c423d 100644 --- a/frontend/src/pages/projects/ProjectViewRoutes.tsx +++ b/frontend/src/pages/projects/ProjectViewRoutes.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { Navigate, Routes, Route } from 'react-router-dom'; -import DetailsPageMetricsWrapper from '~/pages/modelServing/screens/projects/DetailsPageMetricsWrapper'; +import ProjectInferenceMetricsWrapper from '~/pages/modelServing/screens/projects/ProjectInferenceMetricsWrapper'; import useModelMetricsEnabled from '~/pages/modelServing/useModelMetricsEnabled'; +import ProjectRuntimeMetricsWrapper from '~/pages/modelServing/screens/projects/ProjectRuntimeMetricsWrapper'; import ProjectDetails from './screens/detail/ProjectDetails'; import ProjectView from './screens/projects/ProjectView'; import ProjectDetailsContextProvider from './ProjectDetailsContext'; @@ -18,12 +19,15 @@ const ProjectViewRoutes: React.FC = () => { } /> } /> } /> - : - } - /> + {modelMetricsEnabled && ( + <> + } + /> + } /> + + )} } /> } /> From 52f85f3c48c54a2ed5b91657fc948b24b55a2c9a Mon Sep 17 00:00:00 2001 From: Alex Creasy Date: Tue, 25 Apr 2023 18:05:18 +0100 Subject: [PATCH 03/17] Trustyai demo phase0 (#1093) * Explainability: Fairness and Bias Metrics (Phase 0) (#1001) (#1006) (#1007) (#1008) - Initial feature set for TrustyAI related UI functionality - Adds tab based navigation to modelServing screen - Adds a bias metrics tab with charts for visualising SPD and DIR metrics - Enhances prometheus query features for accessing TrustyAI data - Enhacements to MetricsChart component making it more configurable * Update key of request name to match trusty backend * Remove unnecessary div and inline style from tooltip * Remove 15 minutes refresh option * Prefer optional prop to type union with undefined * Move function definitions inline * Prefer narrowing over type conversion * Inline tab change handler * Remove toolbar option from ApplicationsPage * Inline domain calculator functions * Move defaultDomainCalculator to utils * Return null instead of undefined * Use threshold label instead of index for key * Add enum for tab keys * Remove magic numbers from domain calculations * Make ResponsePredicate mandatory and add predicate to useQueryRangeResourceData --- frontend/src/api/prometheus/serving.ts | 22 ++++- .../api/prometheus/usePrometheusQueryRange.ts | 26 +++-- .../prometheus/useQueryRangeResourceData.ts | 47 +++++++++- frontend/src/pages/ApplicationsPage.tsx | 3 - .../pages/modelServing/ModelServingRoutes.tsx | 2 +- .../modelServing/screens/metrics/BiasTab.tsx | 25 +++++ .../modelServing/screens/metrics/DIRChart.tsx | 42 +++++++++ .../screens/metrics/MetricsChart.tsx | 72 +++++++++++--- .../screens/metrics/MetricsPage.tsx | 10 +- .../screens/metrics/MetricsPageTabs.scss | 8 ++ .../screens/metrics/MetricsPageTabs.tsx | 54 +++++++++++ .../screens/metrics/MetricsPageToolbar.tsx | 2 +- .../metrics/ModelServingMetricsContext.tsx | 4 + .../screens/metrics/PerformanceTab.tsx | 17 ++++ .../modelServing/screens/metrics/SPDChart.tsx | 39 ++++++++ .../screens/metrics/ScheduledMetricSelect.tsx | 34 +++++++ .../screens/metrics/TrustyChart.tsx | 94 +++++++++++++++++++ .../modelServing/screens/metrics/types.ts | 21 +++++ .../modelServing/screens/metrics/utils.ts | 12 ++- .../src/pages/projects/ProjectViewRoutes.tsx | 2 +- frontend/src/types.ts | 20 ++-- 21 files changed, 504 insertions(+), 52 deletions(-) create mode 100644 frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/DIRChart.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.scss create mode 100644 frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/PerformanceTab.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/SPDChart.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/ScheduledMetricSelect.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx 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..76d9b3f9f2 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,10 @@ 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) => responsePredicate(response.data?.response.data)); + }, [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..d04955af67 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? */ @@ -10,17 +15,49 @@ const useQueryRangeResourceData = ( query: string, end: number, timeframe: TimeframeTitle, -): ContextResourceData => - useContextResourceData( - usePrometheusQueryRange( +): ContextResourceData => { + const responsePredicate = React.useCallback( + (data) => data.result?.[0]?.values || [], + [], + ); + return useContextResourceData( + usePrometheusQueryRange( active, '/api/prometheus/serving', query, TimeframeTimeRange[timeframe], end, TimeframeStep[timeframe], + responsePredicate, ), 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/ApplicationsPage.tsx b/frontend/src/pages/ApplicationsPage.tsx index 642834fab5..10d2467ea3 100644 --- a/frontend/src/pages/ApplicationsPage.tsx +++ b/frontend/src/pages/ApplicationsPage.tsx @@ -19,7 +19,6 @@ import { type ApplicationsPageProps = { title: string; breadcrumb?: React.ReactNode; - toolbar?: React.ReactNode; description: React.ReactNode; loaded: boolean; empty: boolean; @@ -35,7 +34,6 @@ type ApplicationsPageProps = { const ApplicationsPage: React.FC = ({ title, breadcrumb, - toolbar, description, loaded, empty, @@ -58,7 +56,6 @@ const ApplicationsPage: React.FC = ({ {headerAction && {headerAction}} - {toolbar} ); 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/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..cf1cee08f2 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/DIRChart.tsx @@ -0,0 +1,42 @@ +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 { MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; + +const DIRChart = () => { + const DEFAULT_MAX_THRESHOLD = 1.2; + const DEFAULT_MIN_THRESHOLD = 0.8; + const PADDING = 0.1; + + 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={(maxYValue) => ({ + y: + maxYValue > DEFAULT_MAX_THRESHOLD + ? [0, maxYValue + PADDING] + : [0, DEFAULT_MAX_THRESHOLD + PADDING], + })} + 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..7086409624 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,12 +26,19 @@ 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, + defaultDomainCalculator, formatToShow, getThresholdData, - createGraphMetricLine, useStableMetrics, } from './utils'; @@ -34,33 +46,44 @@ 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 +117,14 @@ const MetricsChart: React.FC = ({ return ( - {title} + + {title} + {toolbar && ( + + {toolbar} + + )} +
{hasSomeData ? ( @@ -106,7 +136,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 +153,27 @@ const MetricsChart: React.FC = ({ /> - {graphLines.map((line, i) => ( - - ))} + {graphLines.map((line, i) => { + switch (type) { + case MetricsChartTypes.AREA: + return ; + break; + case MetricsChartTypes.LINE: + return ; + break; + default: + return null; + } + })} - {threshold && } + {thresholds.map((t) => ( + + ))} ) : ( 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..df7ccce86a --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Tabs, Tab, TabTitleText } from '@patternfly/react-core'; +import { MetricsTabKeys } from '~/pages/modelServing/screens/metrics/types'; +import PerformanceTab from './PerformanceTab'; +import BiasTab from './BiasTab'; +import './MetricsPageTabs.scss'; + +const MetricsPageTabs: React.FC = () => { + const DEFAULT_TAB = MetricsTabKeys.PERFORMANCE; + + const { tab } = useParams(); + const navigate = useNavigate(); + + React.useEffect(() => { + if (!tab) { + navigate(`./${DEFAULT_TAB}`, { replace: true }); + } + }, [DEFAULT_TAB, navigate, tab]); + + return ( + { + if (typeof tabId === 'string') { + navigate(`../${tabId}`, { relative: 'path' }); + } + }} + isBox={false} + aria-label="Metrics page tabs" + role="region" + className="odh-tabs-fix" + > + 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 ( - + setOpen(!isOpen)} + isOpen={isOpen} + onSelect={(event, selection) => { + if (typeof selection === 'string') { + onSelect(selection); + setOpen(false); + } + }} + selections={selected} + > + {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..bc25d18059 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx @@ -0,0 +1,94 @@ +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?.requestName) { + x.metric.requestName = `Payload ${i}`; + } + }); + + React.useEffect(() => { + if (!selectedPayloadName) { + setSelectedPayloadName(metricData[0]?.metric?.requestName); + } + }, [selectedPayloadName, metricData]); + + const payloadOptions: string[] = metricData.map((payload) => payload.metric.requestName); + + const selectedPayload = metricData.find((x) => x.metric.requestName === 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..a8a5933035 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,24 @@ 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, +} + +export enum MetricsTabKeys { + PERFORMANCE = 'performance', + BIAS = 'bias', +} diff --git a/frontend/src/pages/modelServing/screens/metrics/utils.ts b/frontend/src/pages/modelServing/screens/metrics/utils.ts index b22c9c2e16..c22e9eb20f 100644 --- a/frontend/src/pages/modelServing/screens/metrics/utils.ts +++ b/frontend/src/pages/modelServing/screens/metrics/utils.ts @@ -5,6 +5,7 @@ import { TimeframeTitle } from '~/pages/modelServing/screens/types'; import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; import { DashboardConfig } from '~/types'; import { + DomainCalculator, GraphMetricLine, GraphMetricPoint, MetricChartLine, @@ -41,10 +42,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 +122,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 +147,9 @@ export const useStableMetrics = ( ) { metricsRef.current = metrics; } - return metricsRef.current; }; + +export const defaultDomainCalculator: DomainCalculator = (maxYValue) => ({ + y: maxYValue === 0 ? [0, 1] : [0, maxYValue], +}); 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; }; From 7c4cc3e6fd8bd27c94f47b0bb1a42ed5ef525369 Mon Sep 17 00:00:00 2001 From: Alex Creasy Date: Thu, 1 Jun 2023 18:10:16 +0100 Subject: [PATCH 04/17] TrustyAI Client (#1318) * Add support for insecure http requests in development mode * Adds low level API client for TrustyAI service * Adds TrustyAI high level API and contexts * Get scheme of TrustyAI route from k8s data --- backend/src/utils/httpUtils.ts | 17 ++- frontend/src/api/index.ts | 6 + frontend/src/api/pipelines/custom.ts | 8 +- frontend/src/api/proxyUtils.ts | 33 ++++- frontend/src/api/trustyai/custom.ts | 53 +++++++ frontend/src/api/trustyai/k8s.ts | 8 ++ frontend/src/api/trustyai/rawTypes.ts | 48 +++++++ .../explainability/ExplainabilityContext.tsx | 132 ++++++++++++++++++ frontend/src/concepts/explainability/const.ts | 1 + frontend/src/concepts/explainability/types.ts | 48 +++++++ .../useExplainabilityModelData.ts | 31 ++++ .../explainability/useTrustyAPIRoute.ts | 53 +++++++ .../explainability/useTrustyAPIState.ts | 37 +++++ .../explainability/useTrustyAiNamespaceCR.ts | 20 +++ frontend/src/concepts/explainability/utils.ts | 17 +++ .../pipelines/context/PipelinesContext.tsx | 10 +- .../concepts/pipelines/context/useAPIState.ts | 81 ----------- .../pipelines/context/usePipelineAPIState.ts | 60 ++++++++ frontend/src/concepts/proxy/types.ts | 6 + frontend/src/concepts/proxy/useAPIState.ts | 37 +++++ frontend/src/k8sTypes.ts | 12 ++ .../pages/modelServing/ModelServingRoutes.tsx | 20 +-- .../metrics/GlobalInferenceMetricsWrapper.tsx | 5 +- .../screens/metrics/MetricsPage.tsx | 1 - .../screens/metrics/TrustyChart.tsx | 1 + .../ProjectInferenceMetricsWrapper.tsx | 5 +- .../projects/ProjectRuntimeMetricsWrapper.tsx | 5 +- .../src/pages/projects/ProjectViewRoutes.tsx | 8 +- 28 files changed, 643 insertions(+), 120 deletions(-) create mode 100644 frontend/src/api/trustyai/custom.ts create mode 100644 frontend/src/api/trustyai/k8s.ts create mode 100644 frontend/src/api/trustyai/rawTypes.ts create mode 100644 frontend/src/concepts/explainability/ExplainabilityContext.tsx create mode 100644 frontend/src/concepts/explainability/const.ts create mode 100644 frontend/src/concepts/explainability/types.ts create mode 100644 frontend/src/concepts/explainability/useExplainabilityModelData.ts create mode 100644 frontend/src/concepts/explainability/useTrustyAPIRoute.ts create mode 100644 frontend/src/concepts/explainability/useTrustyAPIState.ts create mode 100644 frontend/src/concepts/explainability/useTrustyAiNamespaceCR.ts create mode 100644 frontend/src/concepts/explainability/utils.ts delete mode 100644 frontend/src/concepts/pipelines/context/useAPIState.ts create mode 100644 frontend/src/concepts/pipelines/context/usePipelineAPIState.ts create mode 100644 frontend/src/concepts/proxy/types.ts create mode 100644 frontend/src/concepts/proxy/useAPIState.ts diff --git a/backend/src/utils/httpUtils.ts b/backend/src/utils/httpUtils.ts index 6b7ae051c2..87c2db180b 100644 --- a/backend/src/utils/httpUtils.ts +++ b/backend/src/utils/httpUtils.ts @@ -1,6 +1,8 @@ import https from 'https'; +import http from 'http'; import { getDirectCallOptions } from './directCallUtils'; import { KubeFastifyInstance, OauthFastifyRequest } from '../types'; +import { DEV_MODE } from './constants'; export enum ProxyErrorType { /** Failed during startup */ @@ -65,7 +67,20 @@ export const proxyCall = ( fastify.log.info(`Making ${method} proxy request to ${url}`); - const httpsRequest = https + const web = (url: string) => { + if (url.startsWith('http:')) { + if (!DEV_MODE) { + throw new ProxyError( + ProxyErrorType.SETUP_FAILURE, + 'Insecure HTTP requests are prohibited when not in development mode.', + ); + } + return http; + } + return https; + }; + + const httpsRequest = web(url) .request(url, { method, ...requestOptions }, (res) => { let data = ''; res diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 89261907fa..0270534c82 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -31,3 +31,9 @@ export * from './errorUtils'; // User access review hook export * from './useAccessReview'; + +// Explainability + +export * from './trustyai/custom'; +export * from './trustyai/rawTypes'; +export * from './trustyai/k8s'; diff --git a/frontend/src/api/pipelines/custom.ts b/frontend/src/api/pipelines/custom.ts index 2a704a2a41..959dd65b31 100644 --- a/frontend/src/api/pipelines/custom.ts +++ b/frontend/src/api/pipelines/custom.ts @@ -47,13 +47,15 @@ export const getPipelineRunJob: GetPipelineRunJobAPI = (hostPath) => (opts, pipe handlePipelineFailures(proxyGET(hostPath, `/apis/v1beta1/jobs/${pipelineRunJobId}`, {}, opts)); export const deletePipeline: DeletePipelineAPI = (hostPath) => (opts, pipelineId) => - handlePipelineFailures(proxyDELETE(hostPath, `/apis/v1beta1/pipelines/${pipelineId}`, {}, opts)); + handlePipelineFailures( + proxyDELETE(hostPath, `/apis/v1beta1/pipelines/${pipelineId}`, {}, {}, opts), + ); export const deletePipelineRun: DeletePipelineRunAPI = (hostPath) => (opts, runId) => - handlePipelineFailures(proxyDELETE(hostPath, `/apis/v1beta1/runs/${runId}`, {}, opts)); + handlePipelineFailures(proxyDELETE(hostPath, `/apis/v1beta1/runs/${runId}`, {}, {}, opts)); export const deletePipelineRunJob: DeletePipelineRunJobAPI = (hostPath) => (opts, jobId) => - handlePipelineFailures(proxyDELETE(hostPath, `/apis/v1beta1/jobs/${jobId}`, {}, opts)); + handlePipelineFailures(proxyDELETE(hostPath, `/apis/v1beta1/jobs/${jobId}`, {}, {}, opts)); export const listExperiments: ListExperimentsAPI = (hostPath) => (opts) => handlePipelineFailures( diff --git a/frontend/src/api/proxyUtils.ts b/frontend/src/api/proxyUtils.ts index 11ab3efd53..c7366d37b8 100644 --- a/frontend/src/api/proxyUtils.ts +++ b/frontend/src/api/proxyUtils.ts @@ -4,6 +4,7 @@ import { EitherOrNone } from '~/typeHelpers'; type CallProxyJSONOptions = { queryParams?: Record; + parseJSON?: boolean; } & EitherOrNone< { fileContents: string; @@ -17,7 +18,7 @@ const callProxyJSON = ( host: string, path: string, requestInit: RequestInit, - { data, fileContents, queryParams }: CallProxyJSONOptions, + { data, fileContents, queryParams, parseJSON = true }: CallProxyJSONOptions, ): Promise => { const { method, ...otherOptions } = requestInit; @@ -36,7 +37,14 @@ const callProxyJSON = ( data, fileContents, }), - }).then((response) => response.text().then((data) => JSON.parse(data))); + }).then((response) => + response.text().then((data) => { + if (parseJSON) { + return JSON.parse(data); + } + return data; + }), + ); }; export const proxyGET = ( @@ -45,7 +53,10 @@ export const proxyGET = ( queryParams: Record = {}, options?: K8sAPIOptions, ): Promise => - callProxyJSON(host, path, mergeRequestInit(options, { method: 'GET' }), { queryParams }); + callProxyJSON(host, path, mergeRequestInit(options, { method: 'GET' }), { + queryParams, + parseJSON: options?.parseJSON, + }); /** Standard POST */ export const proxyCREATE = ( @@ -58,6 +69,7 @@ export const proxyCREATE = ( callProxyJSON(host, path, mergeRequestInit(options, { method: 'POST' }), { data, queryParams, + parseJSON: options?.parseJSON, }); /** POST -- but with file content instead of body data */ @@ -71,6 +83,7 @@ export const proxyFILE = ( callProxyJSON(host, path, mergeRequestInit(options, { method: 'POST' }), { fileContents, queryParams, + parseJSON: options?.parseJSON, }); /** POST -- but no body data -- targets simple endpoints */ @@ -82,6 +95,7 @@ export const proxyENDPOINT = ( ): Promise => callProxyJSON(host, path, mergeRequestInit(options, { method: 'POST' }), { queryParams, + parseJSON: options?.parseJSON, }); export const proxyUPDATE = ( @@ -91,12 +105,21 @@ export const proxyUPDATE = ( queryParams: Record = {}, options?: K8sAPIOptions, ): Promise => - callProxyJSON(host, path, mergeRequestInit(options, { method: 'PUT' }), { data, queryParams }); + callProxyJSON(host, path, mergeRequestInit(options, { method: 'PUT' }), { + data, + queryParams, + parseJSON: options?.parseJSON, + }); export const proxyDELETE = ( host: string, path: string, + data: Record, queryParams: Record = {}, options?: K8sAPIOptions, ): Promise => - callProxyJSON(host, path, mergeRequestInit(options, { method: 'DELETE' }), { queryParams }); + callProxyJSON(host, path, mergeRequestInit(options, { method: 'DELETE' }), { + data, + queryParams, + parseJSON: options?.parseJSON, + }); diff --git a/frontend/src/api/trustyai/custom.ts b/frontend/src/api/trustyai/custom.ts new file mode 100644 index 0000000000..f361e4c14f --- /dev/null +++ b/frontend/src/api/trustyai/custom.ts @@ -0,0 +1,53 @@ +import { proxyCREATE, proxyDELETE, proxyGET } from '~/api/proxyUtils'; +import { K8sAPIOptions } from '~/k8sTypes'; +import { BaseMetricCreationResponse, BaseMetricListResponse, BaseMetricRequest } from './rawTypes'; + +export const getInfo = (hostPath: string) => (opts: K8sAPIOptions) => + proxyGET(hostPath, '/info', {}, opts); + +export const getAllRequests = + (hostPath: string) => + (opts: K8sAPIOptions): Promise => + proxyGET(hostPath, '/metrics/all/requests', {}, opts); + +export const getSpdRequests = + (hostPath: string) => + (opts: K8sAPIOptions): Promise => + proxyGET(hostPath, '/metrics/spd/requests', {}, opts); + +export const createSpdRequest = + (hostPath: string) => + (opts: K8sAPIOptions, data: BaseMetricRequest): Promise => + proxyCREATE(hostPath, '/metrics/spd/request', data, {}, opts); + +export const deleteSpdRequest = + (hostPath: string) => + (opts: K8sAPIOptions, id: string): Promise => + proxyDELETE( + hostPath, + '/metrics/spd/request', + { requestId: id }, + {}, + { parseJSON: false, ...opts }, + ); + +export const getDirRequests = + (hostPath: string) => + (opts: K8sAPIOptions): Promise => + proxyGET(hostPath, '/metrics/dir/requests', {}, opts); + +export const createDirRequest = + (hostPath: string) => + (opts: K8sAPIOptions, data: BaseMetricRequest): Promise => + proxyCREATE(hostPath, '/metrics/dir/request', data, {}, opts); + +export const deleteDirRequest = + (hostPath: string) => + (opts: K8sAPIOptions, id: string): Promise => + proxyDELETE( + hostPath, + '/metrics/dir/request', + { requestId: id }, + {}, + { parseJSON: false, ...opts }, + ); diff --git a/frontend/src/api/trustyai/k8s.ts b/frontend/src/api/trustyai/k8s.ts new file mode 100644 index 0000000000..f67e4a1b8c --- /dev/null +++ b/frontend/src/api/trustyai/k8s.ts @@ -0,0 +1,8 @@ +import { K8sAPIOptions, RouteKind } from '~/k8sTypes'; +import { getRoute } from '~/api'; +import { TRUSTYAI_ROUTE_NAME } from '~/concepts/explainability/const'; + +export const getTrustyAIAPIRoute = async ( + namespace: string, + opts?: K8sAPIOptions, +): Promise => getRoute(TRUSTYAI_ROUTE_NAME, namespace, opts); diff --git a/frontend/src/api/trustyai/rawTypes.ts b/frontend/src/api/trustyai/rawTypes.ts new file mode 100644 index 0000000000..0bb41cb47b --- /dev/null +++ b/frontend/src/api/trustyai/rawTypes.ts @@ -0,0 +1,48 @@ +export enum DataTypes { + BOOL, + FLOAT, + DOUBLE, + INT32, + INT64, + STRING, +} + +export enum MetricTypes { + SPD, + DIR, +} + +export type TypedValue = { + type: DataTypes; + value: string; +}; + +export type BaseMetricRequest = { + protectedAttribute: string; + favorableOutcome: TypedValue; + outcomeName: string; + privilegedAttribute: TypedValue; + unprivilegedAttribute: TypedValue; + modelId: string; + requestName: string; + thresholdDelta?: number; + batchSize?: number; +}; + +export type BaseMetricResponse = { + id: string; + request: BaseMetricRequest & { metricName: MetricTypes }; +}; + +export type BaseMetricListResponse = { + requests: BaseMetricResponse[]; +}; + +export type BaseMetricCreationResponse = { + requestId: string; + timestamp: string; +}; + +export type BaseMetricDeletionRequest = { + requestId: string; +}; diff --git a/frontend/src/concepts/explainability/ExplainabilityContext.tsx b/frontend/src/concepts/explainability/ExplainabilityContext.tsx new file mode 100644 index 0000000000..6ce7c43db2 --- /dev/null +++ b/frontend/src/concepts/explainability/ExplainabilityContext.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import useTrustyAPIRoute from '~/concepts/explainability/useTrustyAPIRoute'; +import useTrustyAiNamespaceCR from '~/concepts/explainability/useTrustyAiNamespaceCR'; +import { useDashboardNamespace } from '~/redux/selectors'; +import useTrustyAPIState, { TrustyAPIState } from '~/concepts/explainability/useTrustyAPIState'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; +import { formatListResponse } from '~/concepts/explainability/utils'; +import useFetchState, { + FetchState, + FetchStateCallbackPromise, + NotReadyError, +} from '~/utilities/useFetchState'; + +// TODO create component for ensuring API availability, see pipelines for example. + +type ExplainabilityContextData = { + refresh: () => Promise; + biasMetricConfigs: BiasMetricConfig[]; + loaded: boolean; + error?: Error; +}; + +const defaultExplainabilityContextData: ExplainabilityContextData = { + refresh: () => Promise.resolve(), + biasMetricConfigs: [], + loaded: false, +}; + +type ExplainabilityContextProps = { + hasCR: boolean; + crInitializing: boolean; + serverTimedOut: boolean; + ignoreTimedOut: () => void; + refreshState: () => Promise; + refreshAPIState: () => void; + apiState: TrustyAPIState; + data: ExplainabilityContextData; +}; + +export const ExplainabilityContext = React.createContext({ + hasCR: false, + crInitializing: false, + serverTimedOut: false, + ignoreTimedOut: () => undefined, + data: defaultExplainabilityContextData, + refreshState: async () => undefined, + refreshAPIState: () => undefined, + apiState: { apiAvailable: false, api: null as unknown as TrustyAPIState['api'] }, +}); + +export const ExplainabilityProvider: React.FC = () => { + //TODO: when TrustyAI operator is ready, we will need to use the current DSProject namespace instead. + const namespace = useDashboardNamespace().dashboardNamespace; + + const state = useTrustyAiNamespaceCR(namespace); + //TODO handle CR loaded error - when TIA operator is ready + const [explainabilityNamespaceCR, crLoaded, , refreshCR] = state; + const isCRReady = crLoaded; + //TODO: needs logic to handle server timeouts - when TIA operator is ready + const serverTimedOut = false; + const ignoreTimedOut = React.useCallback(() => true, []); + + //TODO handle routeLoadedError - when TIA operator is ready + const [routeHost, routeLoaded, , refreshRoute] = useTrustyAPIRoute(isCRReady, namespace); + + const hostPath = routeLoaded && routeHost ? routeHost : null; + + const refreshState = React.useCallback( + () => Promise.all([refreshCR(), refreshRoute()]).then(() => undefined), + [refreshRoute, refreshCR], + ); + + const [apiState, refreshAPIState] = useTrustyAPIState(hostPath); + + const data = useFetchContextData(apiState); + + return ( + + + + ); +}; + +//TODO handle errors. +const useFetchContextData = (apiState: TrustyAPIState): ExplainabilityContextData => { + const [biasMetricConfigs, biasMetricConfigsLoaded, , refreshBiasMetricConfigs] = + useFetchBiasMetricConfigs(apiState); + + const refresh = React.useCallback( + () => Promise.all([refreshBiasMetricConfigs()]).then(() => undefined), + [refreshBiasMetricConfigs], + ); + + const loaded = React.useMemo(() => biasMetricConfigsLoaded, [biasMetricConfigsLoaded]); + + return { + biasMetricConfigs, + refresh, + loaded, + }; +}; + +const useFetchBiasMetricConfigs = (apiState: TrustyAPIState): FetchState => { + const callback = React.useCallback>( + (opts) => { + if (!apiState.apiAvailable) { + return Promise.reject(new NotReadyError('API not yet available')); + } + return apiState.api + .listRequests(opts) + .then((r) => formatListResponse(r)) + .catch((e) => { + throw e; + }); + }, + [apiState.api, apiState.apiAvailable], + ); + + return useFetchState(callback, [], { initialPromisePurity: true }); +}; diff --git a/frontend/src/concepts/explainability/const.ts b/frontend/src/concepts/explainability/const.ts new file mode 100644 index 0000000000..b1fea45c9d --- /dev/null +++ b/frontend/src/concepts/explainability/const.ts @@ -0,0 +1 @@ +export const TRUSTYAI_ROUTE_NAME = 'trustyai'; diff --git a/frontend/src/concepts/explainability/types.ts b/frontend/src/concepts/explainability/types.ts new file mode 100644 index 0000000000..1066661dfc --- /dev/null +++ b/frontend/src/concepts/explainability/types.ts @@ -0,0 +1,48 @@ +import { + BaseMetricCreationResponse, + BaseMetricListResponse, + BaseMetricRequest, + MetricTypes, +} from '~/api'; +import { K8sAPIOptions } from '~/k8sTypes'; + +//TODO refine return types +export type GetInfo = (opts: K8sAPIOptions) => Promise; +export type ListRequests = (opts: K8sAPIOptions) => Promise; +export type ListSpdRequests = (opts: K8sAPIOptions) => Promise; +export type ListDirRequests = (opts: K8sAPIOptions) => Promise; +export type CreateSpdRequest = ( + opts: K8sAPIOptions, + x: BaseMetricRequest, +) => Promise; +export type CreateDirRequest = ( + opts: K8sAPIOptions, + x: BaseMetricRequest, +) => Promise; +export type DeleteSpdRequest = (opts: K8sAPIOptions, requestId: string) => Promise; +export type DeleteDirRequest = (opts: K8sAPIOptions, requestId: string) => Promise; + +export type ExplainabilityAPI = { + getInfo: GetInfo; + listRequests: ListRequests; + listSpdRequests: ListSpdRequests; + listDirRequests: ListDirRequests; + createSpdRequest: CreateSpdRequest; + createDirRequest: CreateDirRequest; + deleteSpdRequest: DeleteSpdRequest; + deleteDirRequest: DeleteDirRequest; +}; + +export type BiasMetricConfig = { + id: string; + name: string; + metricType: MetricTypes; + protectedAttribute: string; + outcomeName: string; + favorableOutcome: string; + privilegedAttribute: string; + unprivilegedAttribute: string; + modelId: string; + thresholdDelta?: number; + batchSize?: number; +}; diff --git a/frontend/src/concepts/explainability/useExplainabilityModelData.ts b/frontend/src/concepts/explainability/useExplainabilityModelData.ts new file mode 100644 index 0000000000..17e77dc851 --- /dev/null +++ b/frontend/src/concepts/explainability/useExplainabilityModelData.ts @@ -0,0 +1,31 @@ +import { useParams } from 'react-router-dom'; +import React from 'react'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; +import { ExplainabilityContext } from '~/concepts/explainability/ExplainabilityContext'; + +export type ExplainabilityModelData = { + biasMetricConfigs: BiasMetricConfig[]; + loaded: boolean; + refresh: () => Promise; +}; +export const useExplainabilityModelData = (): ExplainabilityModelData => { + const { inferenceService } = useParams(); + + const { data } = React.useContext(ExplainabilityContext); + + const [biasMetricConfigs] = React.useMemo(() => { + let configs: BiasMetricConfig[] = []; + + if (data.loaded) { + configs = data.biasMetricConfigs.filter((x) => x.modelId === inferenceService); + } + + return [configs]; + }, [data.biasMetricConfigs, data.loaded, inferenceService]); + + return { + biasMetricConfigs, + loaded: data.loaded, + refresh: data.refresh, + }; +}; diff --git a/frontend/src/concepts/explainability/useTrustyAPIRoute.ts b/frontend/src/concepts/explainability/useTrustyAPIRoute.ts new file mode 100644 index 0000000000..5d27dae623 --- /dev/null +++ b/frontend/src/concepts/explainability/useTrustyAPIRoute.ts @@ -0,0 +1,53 @@ +import React from 'react'; +import useFetchState, { + FetchState, + FetchStateCallbackPromise, + NotReadyError, +} from '~/utilities/useFetchState'; +import { getTrustyAIAPIRoute } from '~/api/'; +import { RouteKind } from '~/k8sTypes'; +import { FAST_POLL_INTERVAL } from '~/utilities/const'; + +type State = string | null; +const useTrustyAPIRoute = (hasCR: boolean, namespace: string): FetchState => { + const callback = React.useCallback>( + (opts) => { + if (!hasCR) { + return Promise.reject(new NotReadyError('CR not created')); + } + + //TODO: API URI must use HTTPS before release. + return getTrustyAIAPIRoute(namespace, opts) + .then((result: RouteKind) => `${result.spec.port.targetPort}://${result.spec.host}`) + .catch((e) => { + if (e.statusObject?.code === 404) { + // Not finding is okay, not an error + return null; + } + throw e; + }); + }, + [hasCR, namespace], + ); + + // TODO: add duplicate functionality to useFetchState. + const state = useFetchState(callback, null, { + initialPromisePurity: true, + }); + + const [data, , , refresh] = state; + + const hasData = !!data; + React.useEffect(() => { + let interval; + if (!hasData) { + interval = setInterval(refresh, FAST_POLL_INTERVAL); + } + return () => { + clearInterval(interval); + }; + }, [hasData, refresh]); + return state; +}; + +export default useTrustyAPIRoute; diff --git a/frontend/src/concepts/explainability/useTrustyAPIState.ts b/frontend/src/concepts/explainability/useTrustyAPIState.ts new file mode 100644 index 0000000000..b3291b1c29 --- /dev/null +++ b/frontend/src/concepts/explainability/useTrustyAPIState.ts @@ -0,0 +1,37 @@ +import React from 'react'; +import { APIState } from '~/concepts/proxy/types'; +import { ExplainabilityAPI } from '~/concepts/explainability/types'; +import useAPIState from '~/concepts/proxy/useAPIState'; +import { + createDirRequest, + createSpdRequest, + deleteDirRequest, + getAllRequests, + getDirRequests, + getInfo, + getSpdRequests, +} from '~/api'; + +export type TrustyAPIState = APIState; + +const useTrustyAPIState = ( + hostPath: string | null, +): [apiState: TrustyAPIState, refreshAPIState: () => void] => { + const createAPI = React.useCallback( + (path) => ({ + createDirRequest: createDirRequest(path), + createSpdRequest: createSpdRequest(path), + deleteDirRequest: deleteDirRequest(path), + deleteSpdRequest: deleteDirRequest(path), + getInfo: getInfo(path), + listDirRequests: getDirRequests(path), + listRequests: getAllRequests(path), + listSpdRequests: getSpdRequests(path), + }), + [], + ); + + return useAPIState(hostPath, createAPI); +}; + +export default useTrustyAPIState; diff --git a/frontend/src/concepts/explainability/useTrustyAiNamespaceCR.ts b/frontend/src/concepts/explainability/useTrustyAiNamespaceCR.ts new file mode 100644 index 0000000000..d3e2ac702e --- /dev/null +++ b/frontend/src/concepts/explainability/useTrustyAiNamespaceCR.ts @@ -0,0 +1,20 @@ +import React from 'react'; +import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; +import { TrustyAiKind } from '~/k8sTypes'; + +type State = TrustyAiKind | null; +const useTrustyAiNamespaceCR = (namespace: string): FetchState => { + // TODO: the logic needs to be fleshed out once the TrustyAI operator is complete. + const callback = React.useCallback>( + (opts) => (namespace && opts ? Promise.resolve(null) : Promise.reject()), + [namespace], + ); + + const state = useFetchState(callback, null, { + initialPromisePurity: true, + }); + + return state; +}; + +export default useTrustyAiNamespaceCR; diff --git a/frontend/src/concepts/explainability/utils.ts b/frontend/src/concepts/explainability/utils.ts new file mode 100644 index 0000000000..d6160a8403 --- /dev/null +++ b/frontend/src/concepts/explainability/utils.ts @@ -0,0 +1,17 @@ +import { BaseMetricListResponse } from '~/api'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; + +export const formatListResponse = (x: BaseMetricListResponse): BiasMetricConfig[] => + x.requests.map((m) => ({ + batchSize: m.request.batchSize, + favorableOutcome: m.request.favorableOutcome.value, + id: m.id, + metricType: m.request.metricName, + modelId: m.request.modelId, + name: m.request.requestName, + outcomeName: m.request.outcomeName, + privilegedAttribute: m.request.privilegedAttribute.value, + protectedAttribute: m.request.protectedAttribute, + thresholdDelta: m.request.thresholdDelta, + unprivilegedAttribute: m.request.unprivilegedAttribute.value, + })); diff --git a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx index 415cb6e85b..855b8c8f24 100644 --- a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx +++ b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx @@ -16,7 +16,7 @@ import ViewPipelineServerModal from '~/concepts/pipelines/content/ViewPipelineSe import useSyncPreferredProject from '~/concepts/projects/useSyncPreferredProject'; import useManageElyraSecret from '~/concepts/pipelines/context/useManageElyraSecret'; import { deleteServer } from '~/concepts/pipelines/utils'; -import useAPIState, { APIState } from './useAPIState'; +import usePipelineAPIState, { PipelineAPIState } from './usePipelineAPIState'; import usePipelineNamespaceCR, { dspaLoaded, hasServerTimedOut } from './usePipelineNamespaceCR'; import usePipelinesAPIRoute from './usePipelinesAPIRoute'; @@ -29,7 +29,7 @@ type PipelineContext = { project: ProjectKind; refreshState: () => Promise; refreshAPIState: () => void; - apiState: APIState; + apiState: PipelineAPIState; }; const PipelinesContext = React.createContext({ @@ -41,7 +41,7 @@ const PipelinesContext = React.createContext({ project: null as unknown as ProjectKind, refreshState: async () => undefined, refreshAPIState: () => undefined, - apiState: { apiAvailable: false, api: null as unknown as APIState['api'] }, + apiState: { apiAvailable: false, api: null as unknown as PipelineAPIState['api'] }, }); type PipelineContextProviderProps = { @@ -78,7 +78,7 @@ export const PipelineContextProvider: React.FC = ( [refreshRoute, refreshCR], ); - const [apiState, refreshAPIState] = useAPIState(hostPath); + const [apiState, refreshAPIState] = usePipelineAPIState(hostPath); let error = crLoadError || routeLoadError; if (error || !project) { @@ -111,7 +111,7 @@ export const PipelineContextProvider: React.FC = ( ); }; -type UsePipelinesAPI = APIState & { +type UsePipelinesAPI = PipelineAPIState & { /** The contextual namespace */ namespace: string; /** The Project resource behind the namespace */ diff --git a/frontend/src/concepts/pipelines/context/useAPIState.ts b/frontend/src/concepts/pipelines/context/useAPIState.ts deleted file mode 100644 index 9ad72b964f..0000000000 --- a/frontend/src/concepts/pipelines/context/useAPIState.ts +++ /dev/null @@ -1,81 +0,0 @@ -import * as React from 'react'; -import { - createExperiment, - createPipelineRun, - createPipelineRunJob, - deletePipeline, - deletePipelineRun, - deletePipelineRunJob, - getExperiment, - getPipeline, - getPipelineRun, - getPipelineRunJob, - listExperiments, - listPipelineRunJobs, - listPipelineRuns, - listPipelineRunsByPipeline, - listPipelines, - listPipelineTemplates, - stopPipelineRun, - updatePipelineRunJob, - uploadPipeline, -} from '~/api'; -import { PipelineAPIs } from '~/concepts/pipelines/types'; - -export type APIState = { - /** If API will successfully call */ - apiAvailable: boolean; - /** The available API functions */ - api: PipelineAPIs; -}; - -const useAPIState = ( - hostPath: string | null, -): [apiState: APIState, refreshAPIState: () => void] => { - const [internalAPIToggleState, setInternalAPIToggleState] = React.useState(false); - - const refreshAPIState = React.useCallback(() => { - setInternalAPIToggleState((v) => !v); - }, []); - - const apiState = React.useMemo(() => { - // Note: This is a hack usage to get around the linter -- avoid copying this logic - // eslint-disable-next-line no-console - console.log('Computing Pipeline API', internalAPIToggleState ? '' : ''); - - let path = hostPath; - if (!path) { - // TODO: we need to figure out maybe a stopgap or something - path = ''; - } - - return { - apiAvailable: !!path, - api: { - createExperiment: createExperiment(path), - createPipelineRun: createPipelineRun(path), - createPipelineRunJob: createPipelineRunJob(path), - getExperiment: getExperiment(path), - getPipeline: getPipeline(path), - getPipelineRun: getPipelineRun(path), - getPipelineRunJob: getPipelineRunJob(path), - deletePipeline: deletePipeline(path), - deletePipelineRun: deletePipelineRun(path), - deletePipelineRunJob: deletePipelineRunJob(path), - listExperiments: listExperiments(path), - listPipelines: listPipelines(path), - listPipelineRuns: listPipelineRuns(path), - listPipelineRunJobs: listPipelineRunJobs(path), - listPipelineRunsByPipeline: listPipelineRunsByPipeline(path), - listPipelineTemplate: listPipelineTemplates(path), - stopPipelineRun: stopPipelineRun(path), - updatePipelineRunJob: updatePipelineRunJob(path), - uploadPipeline: uploadPipeline(path), - }, - }; - }, [hostPath, internalAPIToggleState]); - - return [apiState, refreshAPIState]; -}; - -export default useAPIState; diff --git a/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts b/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts new file mode 100644 index 0000000000..705f0388c0 --- /dev/null +++ b/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts @@ -0,0 +1,60 @@ +import React from 'react'; +import { + createExperiment, + createPipelineRun, + createPipelineRunJob, + deletePipeline, + deletePipelineRun, + deletePipelineRunJob, + getExperiment, + getPipeline, + getPipelineRun, + getPipelineRunJob, + listExperiments, + listPipelineRunJobs, + listPipelineRuns, + listPipelineRunsByPipeline, + listPipelines, + listPipelineTemplates, + stopPipelineRun, + updatePipelineRunJob, + uploadPipeline, +} from '~/api'; +import { PipelineAPIs } from '~/concepts/pipelines/types'; +import { APIState } from '~/concepts/proxy/types'; +import useAPIState from '~/concepts/proxy/useAPIState'; + +export type PipelineAPIState = APIState; + +const usePipelineAPIState = ( + hostPath: string | null, +): [apiState: PipelineAPIState, refreshAPIState: () => void] => { + const createAPI = React.useCallback( + (path) => ({ + createExperiment: createExperiment(path), + createPipelineRun: createPipelineRun(path), + createPipelineRunJob: createPipelineRunJob(path), + getExperiment: getExperiment(path), + getPipeline: getPipeline(path), + getPipelineRun: getPipelineRun(path), + getPipelineRunJob: getPipelineRunJob(path), + deletePipeline: deletePipeline(path), + deletePipelineRun: deletePipelineRun(path), + deletePipelineRunJob: deletePipelineRunJob(path), + listExperiments: listExperiments(path), + listPipelines: listPipelines(path), + listPipelineRuns: listPipelineRuns(path), + listPipelineRunJobs: listPipelineRunJobs(path), + listPipelineRunsByPipeline: listPipelineRunsByPipeline(path), + listPipelineTemplate: listPipelineTemplates(path), + stopPipelineRun: stopPipelineRun(path), + updatePipelineRunJob: updatePipelineRunJob(path), + uploadPipeline: uploadPipeline(path), + }), + [], + ); + + return useAPIState(hostPath, createAPI); +}; + +export default usePipelineAPIState; diff --git a/frontend/src/concepts/proxy/types.ts b/frontend/src/concepts/proxy/types.ts new file mode 100644 index 0000000000..7e23db7d72 --- /dev/null +++ b/frontend/src/concepts/proxy/types.ts @@ -0,0 +1,6 @@ +export type APIState = { + /** If API will successfully call */ + apiAvailable: boolean; + /** The available API functions */ + api: T; +}; diff --git a/frontend/src/concepts/proxy/useAPIState.ts b/frontend/src/concepts/proxy/useAPIState.ts new file mode 100644 index 0000000000..d3da7b56c9 --- /dev/null +++ b/frontend/src/concepts/proxy/useAPIState.ts @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { APIState } from '~/concepts/proxy/types'; + +//TODO move this to new folder called: proxy +const useAPIState = ( + hostPath: string | null, + createAPI: (path: string) => T, +): [apiState: APIState, refreshAPIState: () => void] => { + const [internalAPIToggleState, setInternalAPIToggleState] = React.useState(false); + + const refreshAPIState = React.useCallback(() => { + setInternalAPIToggleState((v) => !v); + }, []); + + const apiState = React.useMemo>(() => { + // Note: This is a hack usage to get around the linter -- avoid copying this logic + // eslint-disable-next-line no-console + console.log('Computing API', internalAPIToggleState ? '' : ''); + + let path = hostPath; + if (!path) { + // TODO: we need to figure out maybe a stopgap or something + path = ''; + } + + const api = createAPI(path); + + return { + apiAvailable: !!path, + api, + }; + }, [createAPI, hostPath, internalAPIToggleState]); + + return [apiState, refreshAPIState]; +}; + +export default useAPIState; diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index a351f48435..b4a19e874f 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -197,6 +197,7 @@ export type ImageStreamSpecTagType = { export type K8sAPIOptions = { dryRun?: boolean; signal?: AbortSignal; + parseJSON?: boolean; }; /** A status object when Kube backend can't handle a request. */ @@ -399,6 +400,9 @@ export type RouteKind = K8sResourceCommon & { spec: { host: string; path: string; + port: { + targetPort: string; + }; }; }; @@ -422,6 +426,14 @@ export type AWSSecretKind = SecretKind & { data: Record; }; +//TODO this type needs fleshing out when the Trusty operator is ready. +export type TrustyAiKind = K8sResourceCommon & { + metadata: { + name: string; + namespace: string; + }; +}; + export type DSPipelineKind = K8sResourceCommon & { metadata: { name: string; diff --git a/frontend/src/pages/modelServing/ModelServingRoutes.tsx b/frontend/src/pages/modelServing/ModelServingRoutes.tsx index 9d64266120..1654107de9 100644 --- a/frontend/src/pages/modelServing/ModelServingRoutes.tsx +++ b/frontend/src/pages/modelServing/ModelServingRoutes.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Navigate, Route } from 'react-router-dom'; import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes'; +import { ExplainabilityProvider } from '~/concepts/explainability/ExplainabilityContext'; import ModelServingContextProvider from './ModelServingContext'; import GlobalInferenceMetricsWrapper from './screens/metrics/GlobalInferenceMetricsWrapper'; import ModelServingGlobal from './screens/global/ModelServingGlobal'; @@ -9,18 +10,21 @@ import useModelMetricsEnabled from './useModelMetricsEnabled'; const ModelServingRoutes: React.FC = () => { const [modelMetricsEnabled] = useModelMetricsEnabled(); + //TODO: Split route to project and mount provider here. This will allow you to load data when model switching is later implemented. return ( }> } /> - : - } - /> - {/* TODO: Global Runtime metrics?? */} - } /> + }> + : + } + /> + {/* TODO: Global Runtime metrics?? */} + } /> + ); diff --git a/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx index 987b13dec4..cc825a8b0c 100644 --- a/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx @@ -4,7 +4,6 @@ import { Bullseye, Spinner } from '@patternfly/react-core'; import NotFound from '~/pages/NotFound'; import { ModelServingContext } from '~/pages/modelServing/ModelServingContext'; import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; -import InferenceGraphs from '~/pages/modelServing/screens/metrics/InferenceGraphs'; import { MetricType } from '~/pages/modelServing/screens/types'; import { ModelServingMetricsProvider } from './ModelServingMetricsContext'; import MetricsPage from './MetricsPage'; @@ -45,9 +44,7 @@ const GlobalInferenceMetricsWrapper: React.FC = () => { isActive: true, }, ]} - > - - + /> ); }; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx index ac752c44d2..b6839043c6 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx @@ -7,7 +7,6 @@ import ApplicationsPage from '~/pages/ApplicationsPage'; import MetricsPageTabs from '~/pages/modelServing/screens/metrics/MetricsPageTabs'; type MetricsPageProps = { - children: React.ReactNode; title: string; breadcrumbItems: BreadcrumbItemType[]; }; diff --git a/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx b/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx index bc25d18059..3890555cd3 100644 --- a/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx @@ -73,6 +73,7 @@ const TrustyChart: React.FC = ({ )} + {/* + } + loaded={loaded} + provideChildrenPadding + // The page is not empty, we will handle the empty state in the table + empty={false} + > + + + ); +}; + +export default BiasConfigurationPage; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx new file mode 100644 index 0000000000..4eaa6b4c78 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { Button, ToolbarItem } from '@patternfly/react-core'; +import Table from '~/components/table/Table'; +import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; +import BiasConfigurationTableRow from './BiasConfigurationTableRow'; +import { columns } from './tableData'; +import BiasConfigurationEmptyState from './BiasConfigurationEmptyState'; + +type BiasConfigurationTableProps = { + configurations: BiasMetricConfig[]; +}; + +const BiasConfigurationTable: React.FC = ({ configurations }) => { + const [searchType, setSearchType] = React.useState(SearchType.NAME); + const [search, setSearch] = React.useState(''); + const filteredConfigurations = configurations.filter((configuration) => { + if (!search) { + return true; + } + + // TODO: add more search types + switch (searchType) { + case SearchType.NAME: + return configuration.name.toLowerCase().includes(search.toLowerCase()); + case SearchType.PROTECTED_ATTRIBUTE: + return configuration.protectedAttribute.toLowerCase().includes(search.toLowerCase()); + case SearchType.OUTPUT: + return configuration.outcomeName.toLowerCase().includes(search.toLowerCase()); + default: + return true; + } + }); + + const resetFilters = () => { + setSearch(''); + }; + + // TODO: decide what we want to search + // Or should we reuse the complex filter search + const searchTypes = React.useMemo( + () => + Object.keys(SearchType).filter( + (key) => + SearchType[key] === SearchType.NAME || + SearchType[key] === SearchType.PROTECTED_ATTRIBUTE || + SearchType[key] === SearchType.OUTPUT, + ), + [], + ); + return ( + ( + + )} + emptyTableView={ + search ? ( + <> + No metric configurations match your filters.{' '} + + + ) : ( + + ) + } + toolbarContent={ + <> + + { + setSearchType(searchType); + }} + onSearchValueChange={(searchValue) => { + setSearch(searchValue); + }} + /> + + + {/* TODO: add configure metric action */} + + + + } + /> + ); +}; + +export default BiasConfigurationTable; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx new file mode 100644 index 0000000000..d9a98a4a52 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { ActionsColumn, ExpandableRowContent, Tbody, Td, Tr } from '@patternfly/react-table'; +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; + +type BiasConfigurationTableRowProps = { + obj: BiasMetricConfig; + rowIndex: number; +}; + +const BiasConfigurationTableRow: React.FC = ({ obj, rowIndex }) => { + const [isExpanded, setExpanded] = React.useState(false); + + return ( + + + + + + + + + + + + + + + + ); +}; + +export default BiasConfigurationTableRow; diff --git a/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsPage.tsx new file mode 100644 index 0000000000..bbbf45e1bf --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsPage.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; +import MetricsPage from './MetricsPage'; +import { GlobalInferenceMetricsOutletContextProps } from './GlobalInferenceMetricsWrapper'; + +const GlobalInferenceMetricsPage: React.FC = () => { + const { inferenceService } = useOutletContext(); + const modelDisplayName = getInferenceServiceDisplayName(inferenceService); + return ( + + ); +}; + +export default GlobalInferenceMetricsPage; diff --git a/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx index cc825a8b0c..975f5f7dce 100644 --- a/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/GlobalInferenceMetricsWrapper.tsx @@ -1,52 +1,27 @@ import * as React from 'react'; -import { useParams } from 'react-router-dom'; -import { Bullseye, Spinner } from '@patternfly/react-core'; -import NotFound from '~/pages/NotFound'; -import { ModelServingContext } from '~/pages/modelServing/ModelServingContext'; -import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; +import { Outlet } from 'react-router-dom'; import { MetricType } from '~/pages/modelServing/screens/types'; +import { InferenceServiceKind } from '~/k8sTypes'; +import InferenceMetricsPathWrapper from './InferenceMetricsPathWrapper'; import { ModelServingMetricsProvider } from './ModelServingMetricsContext'; -import MetricsPage from './MetricsPage'; import { getInferenceServiceMetricsQueries } from './utils'; -const GlobalInferenceMetricsWrapper: React.FC = () => { - const { project: projectName, inferenceService: modelName } = useParams<{ - project: string; - inferenceService: string; - }>(); - const { - inferenceServices: { data: models, loaded }, - } = React.useContext(ModelServingContext); - const inferenceService = models.find( - (model) => model.metadata.name === modelName && model.metadata.namespace === projectName, - ); - if (!loaded) { - return ( - - - - ); - } - if (!inferenceService) { - return ; - } - const queries = getInferenceServiceMetricsQueries(inferenceService); - const modelDisplayName = getInferenceServiceDisplayName(inferenceService); - - return ( - - - - ); +export type GlobalInferenceMetricsOutletContextProps = { + inferenceService: InferenceServiceKind; + projectName: string; }; +const GlobalInferenceMetricsWrapper: React.FC = () => ( + + {(inferenceService, projectName) => { + const queries = getInferenceServiceMetricsQueries(inferenceService); + return ( + + + + ); + }} + +); + export default GlobalInferenceMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/metrics/InferenceMetricsPathWrapper.tsx b/frontend/src/pages/modelServing/screens/metrics/InferenceMetricsPathWrapper.tsx new file mode 100644 index 0000000000..a41eefb821 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/InferenceMetricsPathWrapper.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import { ModelServingContext } from '~/pages/modelServing/ModelServingContext'; +import NotFound from '~/pages/NotFound'; +import { InferenceServiceKind } from '~/k8sTypes'; + +type InferenceMetricsPathWrapperProps = { + children: (inferenceService: InferenceServiceKind, projectName: string) => React.ReactNode; +}; + +const InferenceMetricsPathWrapper: React.FC = ({ children }) => { + const { project: projectName, inferenceService: modelName } = useParams<{ + project: string; + inferenceService: string; + }>(); + const { + inferenceServices: { data: models, loaded }, + } = React.useContext(ModelServingContext); + const inferenceService = models.find( + (model) => model.metadata.name === modelName && model.metadata.namespace === projectName, + ); + if (!loaded) { + return ( + + + + ); + } + if (!inferenceService || !projectName) { + return ; + } + + return <>{children(inferenceService, projectName)}; +}; + +export default InferenceMetricsPathWrapper; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx index b6839043c6..a9ebb1f8cf 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; - -import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; -import { Link } from 'react-router-dom'; +import { Breadcrumb } from '@patternfly/react-core'; import { BreadcrumbItemType } from '~/types'; import ApplicationsPage from '~/pages/ApplicationsPage'; import MetricsPageTabs from '~/pages/modelServing/screens/metrics/MetricsPageTabs'; +import { getBreadcrumbItemComponents } from './utils'; type MetricsPageProps = { title: string; @@ -14,19 +13,7 @@ type MetricsPageProps = { const MetricsPage: React.FC = ({ title, breadcrumbItems }) => ( - {breadcrumbItems.map((item) => ( - - item.link ? {item.label} : <>{item.label} - } - /> - ))} - - } + breadcrumb={{getBreadcrumbItemComponents(breadcrumbItems)}} loaded description={null} empty={false} diff --git a/frontend/src/pages/modelServing/screens/metrics/tableData.tsx b/frontend/src/pages/modelServing/screens/metrics/tableData.tsx new file mode 100644 index 0000000000..8b93b5a5cb --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/tableData.tsx @@ -0,0 +1,51 @@ +import { SortableData } from '~/components/table/useTableColumnSort'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; + +// TODO: add sortable +export const columns: SortableData[] = [ + { + field: 'expand', + label: '', + sortable: false, + }, + { + field: 'name', + label: 'Name', + sortable: (a, b) => a.name.localeCompare(b.name), + }, + { + field: 'metric', + label: 'Metric', + sortable: false, + }, + { + field: 'protected-attribute', + label: 'Protected attribute', + sortable: false, + }, + { + field: 'privileged-value', + label: 'Privileged value', + sortable: false, + }, + { + field: 'unprivileged-value', + label: 'Unprivileged value', + sortable: false, + }, + { + field: 'output', + label: 'Output', + sortable: false, + }, + { + field: 'output-value', + label: 'Output value', + sortable: false, + }, + { + field: 'kebab', + label: '', + sortable: false, + }, +]; diff --git a/frontend/src/pages/modelServing/screens/metrics/utils.ts b/frontend/src/pages/modelServing/screens/metrics/utils.tsx similarity index 90% rename from frontend/src/pages/modelServing/screens/metrics/utils.ts rename to frontend/src/pages/modelServing/screens/metrics/utils.tsx index c22e9eb20f..26878160da 100644 --- a/frontend/src/pages/modelServing/screens/metrics/utils.ts +++ b/frontend/src/pages/modelServing/screens/metrics/utils.tsx @@ -1,9 +1,10 @@ import * as _ from 'lodash'; -import { SelectOptionObject } from '@patternfly/react-core'; +import { BreadcrumbItem, SelectOptionObject } from '@patternfly/react-core'; import * as React from 'react'; +import { Link } from 'react-router-dom'; import { TimeframeTitle } from '~/pages/modelServing/screens/types'; import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; -import { DashboardConfig } from '~/types'; +import { BreadcrumbItemType, DashboardConfig } from '~/types'; import { DomainCalculator, GraphMetricLine, @@ -153,3 +154,12 @@ export const useStableMetrics = ( export const defaultDomainCalculator: DomainCalculator = (maxYValue) => ({ y: maxYValue === 0 ? [0, 1] : [0, maxYValue], }); + +export const getBreadcrumbItemComponents = (breadcrumbItems: BreadcrumbItemType[]) => + breadcrumbItems.map((item) => ( + (item.link ? {item.label} : <>{item.label})} + /> + )); diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsConfigurationPage.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsConfigurationPage.tsx new file mode 100644 index 0000000000..f631463176 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsConfigurationPage.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { useOutletContext } from 'react-router'; +import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; +import BiasConfigurationPage from '~/pages/modelServing/screens/metrics/BiasConfigurationPage'; +import { getProjectDisplayName } from '~/pages/projects/utils'; +import { ProjectInferenceMetricsOutletContextProps } from './ProjectInferenceMetricsWrapper'; + +const ProjectInferenceMetricsConfigurationPage: React.FC = () => { + const { currentProject, inferenceService } = + useOutletContext(); + const modelDisplayName = getInferenceServiceDisplayName(inferenceService); + return ( + + ); +}; + +export default ProjectInferenceMetricsConfigurationPage; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsPage.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsPage.tsx new file mode 100644 index 0000000000..fa508d33cc --- /dev/null +++ b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsPage.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; +import MetricsPage from '~/pages/modelServing/screens/metrics/MetricsPage'; +import { getProjectDisplayName } from '~/pages/projects/utils'; +import { ProjectInferenceMetricsOutletContextProps } from './ProjectInferenceMetricsWrapper'; + +const ProjectInferenceMetricsPage: React.FC = () => { + const { inferenceService, currentProject } = + useOutletContext(); + const projectDisplayName = getProjectDisplayName(currentProject); + const modelDisplayName = getInferenceServiceDisplayName(inferenceService); + + return ( + + ); +}; + +export default ProjectInferenceMetricsPage; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsPathWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsPathWrapper.tsx new file mode 100644 index 0000000000..6a70be2049 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsPathWrapper.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import NotFound from '~/pages/NotFound'; +import { InferenceServiceKind, ProjectKind } from '~/k8sTypes'; +import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; + +type ProjectInferenceMetricsPathWrapperProps = { + children: ( + inferenceService: InferenceServiceKind, + currentProject: ProjectKind, + ) => React.ReactNode; +}; + +const ProjectInferenceMetricsPathWrapper: React.FC = ({ + children, +}) => { + const { inferenceService: modelName } = useParams<{ + inferenceService: string; + }>(); + const { + currentProject, + inferenceServices: { data: models, loaded }, + } = React.useContext(ProjectDetailsContext); + const inferenceService = models.find((model) => model.metadata.name === modelName); + if (!loaded) { + return ( + + + + ); + } + if (!inferenceService) { + return ; + } + + return <>{children(inferenceService, currentProject)}; +}; + +export default ProjectInferenceMetricsPathWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsWrapper.tsx index 8793b62a99..de9ac2f8d0 100644 --- a/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsWrapper.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsWrapper.tsx @@ -1,56 +1,27 @@ import * as React from 'react'; -import { useParams } from 'react-router-dom'; -import { Bullseye, Spinner } from '@patternfly/react-core'; -import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; -import MetricsPage from '~/pages/modelServing/screens/metrics/MetricsPage'; +import { Outlet } from 'react-router-dom'; import { ModelServingMetricsProvider } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; import { getInferenceServiceMetricsQueries } from '~/pages/modelServing/screens/metrics/utils'; -import NotFound from '~/pages/NotFound'; -import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; -import { getProjectDisplayName } from '~/pages/projects/utils'; import { MetricType } from '~/pages/modelServing/screens/types'; +import { InferenceServiceKind, ProjectKind } from '~/k8sTypes'; +import ProjectInferenceMetricsPathWrapper from './ProjectInferenceMetricsPathWrapper'; -const ProjectInferenceMetricsWrapper: React.FC = () => { - const { inferenceService: modelName } = useParams<{ - inferenceService: string; - }>(); - const { - currentProject, - inferenceServices: { data: models, loaded }, - } = React.useContext(ProjectDetailsContext); - const inferenceService = models.find((model) => model.metadata.name === modelName); - if (!loaded) { - return ( - - - - ); - } - if (!inferenceService) { - return ; - } - const queries = getInferenceServiceMetricsQueries(inferenceService); - const projectDisplayName = getProjectDisplayName(currentProject); - const modelDisplayName = getInferenceServiceDisplayName(inferenceService); - - return ( - - - - ); +export type ProjectInferenceMetricsOutletContextProps = { + inferenceService: InferenceServiceKind; + currentProject: ProjectKind; }; +const ProjectInferenceMetricsWrapper: React.FC = () => ( + + {(inferenceService, currentProject) => { + const queries = getInferenceServiceMetricsQueries(inferenceService); + return ( + + + + ); + }} + +); + export default ProjectInferenceMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectRuntimeMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectRuntimeMetricsWrapper.tsx index c6ae753349..264d90a812 100644 --- a/frontend/src/pages/modelServing/screens/projects/ProjectRuntimeMetricsWrapper.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ProjectRuntimeMetricsWrapper.tsx @@ -8,7 +8,7 @@ import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; import { getProjectDisplayName } from '~/pages/projects/utils'; import { MetricType } from '~/pages/modelServing/screens/types'; -const ProjectInferenceMetricsWrapper: React.FC = () => { +const ProjectRuntimeMetricsWrapper: React.FC = () => { const { currentProject, servingRuntimes: { data: runtimes, loaded }, @@ -47,4 +47,4 @@ const ProjectInferenceMetricsWrapper: React.FC = () => { ); }; -export default ProjectInferenceMetricsWrapper; +export default ProjectRuntimeMetricsWrapper; diff --git a/frontend/src/pages/projects/ProjectViewRoutes.tsx b/frontend/src/pages/projects/ProjectViewRoutes.tsx index 3d2738a069..9d4e96370c 100644 --- a/frontend/src/pages/projects/ProjectViewRoutes.tsx +++ b/frontend/src/pages/projects/ProjectViewRoutes.tsx @@ -10,6 +10,8 @@ import PipelineRunDetails from '~/concepts/pipelines/content/pipelinesDetails/pi import CreateRunPage from '~/concepts/pipelines/content/createRun/CreateRunPage'; import CloneRunPage from '~/concepts/pipelines/content/createRun/CloneRunPage'; import { ExplainabilityProvider } from '~/concepts/explainability/ExplainabilityContext'; +import ProjectInferenceMetricsConfigurationPage from '~/pages/modelServing/screens/projects/ProjectInferenceMetricsConfigurationPage'; +import ProjectInferenceMetricsPage from '~/pages/modelServing/screens/projects/ProjectInferenceMetricsPage'; import ProjectDetails from './screens/detail/ProjectDetails'; import ProjectView from './screens/projects/ProjectView'; import ProjectDetailsContextProvider from './ProjectDetailsContext'; @@ -29,7 +31,10 @@ const ProjectViewRoutes: React.FC = () => { {modelMetricsEnabled && ( <> }> - } /> + }> + } /> + } /> + } /> diff --git a/frontend/src/pages/projects/screens/projects/ProjectListView.tsx b/frontend/src/pages/projects/screens/projects/ProjectListView.tsx index 9508631424..d4500f8993 100644 --- a/frontend/src/pages/projects/screens/projects/ProjectListView.tsx +++ b/frontend/src/pages/projects/screens/projects/ProjectListView.tsx @@ -3,11 +3,11 @@ import { Button, ButtonVariant, ToolbarItem } from '@patternfly/react-core'; import { useNavigate } from 'react-router-dom'; import Table from '~/components/table/Table'; import useTableColumnSort from '~/components/table/useTableColumnSort'; -import SearchField, { SearchType } from '~/pages/projects/components/SearchField'; import { ProjectKind } from '~/k8sTypes'; import { getProjectDisplayName, getProjectOwner } from '~/pages/projects/utils'; import LaunchJupyterButton from '~/pages/projects/screens/projects/LaunchJupyterButton'; import { ProjectsContext } from '~/concepts/projects/ProjectsContext'; +import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField'; import NewProjectButton from './NewProjectButton'; import { columns } from './tableData'; import ProjectTableRow from './ProjectTableRow'; @@ -44,7 +44,12 @@ const ProjectListView: React.FC = ({ allowCreate }) => { setSearch(''); }; - const searchTypes = React.useMemo(() => Object.keys(SearchType), []); + const searchTypes = Object.keys(SearchType).filter( + (key) => + SearchType[key] === SearchType.NAME || + SearchType[key] === SearchType.PROJECT || + SearchType[key] === SearchType.USER, + ); const [deleteData, setDeleteData] = React.useState(); const [editData, setEditData] = React.useState(); @@ -76,7 +81,7 @@ const ProjectListView: React.FC = ({ allowCreate }) => { toolbarContent={ - Date: Mon, 5 Jun 2023 20:18:30 +0100 Subject: [PATCH 06/17] Update Trusty AI client to handle API changes (#1336) (#1337) --- frontend/src/api/trustyai/rawTypes.ts | 34 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/frontend/src/api/trustyai/rawTypes.ts b/frontend/src/api/trustyai/rawTypes.ts index 0bb41cb47b..780ba0d405 100644 --- a/frontend/src/api/trustyai/rawTypes.ts +++ b/frontend/src/api/trustyai/rawTypes.ts @@ -1,15 +1,15 @@ export enum DataTypes { - BOOL, - FLOAT, - DOUBLE, - INT32, - INT64, - STRING, + BOOL = 'BOOL', + FLOAT = 'FLOAT', + DOUBLE = 'DOUBLE', + INT32 = 'INT32', + INT64 = 'INT64', + STRING = 'STRING', } export enum MetricTypes { - SPD, - DIR, + SPD = 'SPD', + DIR = 'DIR', } export type TypedValue = { @@ -17,21 +17,29 @@ export type TypedValue = { value: string; }; -export type BaseMetricRequest = { +export type BaseMetric = { protectedAttribute: string; - favorableOutcome: TypedValue; outcomeName: string; - privilegedAttribute: TypedValue; - unprivilegedAttribute: TypedValue; modelId: string; requestName: string; thresholdDelta?: number; batchSize?: number; }; +export type BaseMetricRequest = { + favorableOutcome: string; + privilegedAttribute: string; + unprivilegedAttribute: string; +} & BaseMetric; + export type BaseMetricResponse = { id: string; - request: BaseMetricRequest & { metricName: MetricTypes }; + request: { + metricName: MetricTypes; + favorableOutcome: TypedValue; + privilegedAttribute: TypedValue; + unprivilegedAttribute: TypedValue; + } & BaseMetric; }; export type BaseMetricListResponse = { From 4aa8675dc41be45b3c3bff9d2e10a94966cbe43f Mon Sep 17 00:00:00 2001 From: Juntao Wang <37624318+DaoDaoNoCode@users.noreply.github.com> Date: Thu, 8 Jun 2023 09:35:03 -0400 Subject: [PATCH 07/17] Add bias metrics configuration modal (#1343) * Add configuration modal * address comments * get rid of some TODOs and refine the route --- frontend/src/api/prometheus/serving.ts | 13 +- .../dashboard/DashboardModalFooter.tsx | 48 ++++++ .../pages/modelServing/ModelServingRoutes.tsx | 2 + .../metrics/BiasConfigurationAlertPopover.tsx | 31 ++++ .../BiasConfigurationBreadcrumbPage.tsx | 2 +- .../metrics/BiasConfigurationButton.tsx | 40 +++++ .../screens/metrics/BiasConfigurationPage.tsx | 13 +- .../metrics/BiasConfigurationTable.tsx | 125 +++++++++----- .../metrics/BiasConfigurationTableRow.tsx | 27 ++- .../modelServing/screens/metrics/BiasTab.tsx | 49 ++++-- .../metrics/EmptyBiasConfigurationCard.tsx | 39 +++++ .../screens/metrics/MetricsPage.tsx | 43 +++-- .../screens/metrics/MetricsPageTabs.tsx | 22 ++- .../DeleteBiasConfigurationModal.tsx | 59 +++++++ .../ManageBiasConfigurationModal.tsx | 163 ++++++++++++++++++ .../MetricTypeField.tsx | 52 ++++++ .../useBiasConfigurationObject.ts | 68 ++++++++ .../modelServing/screens/metrics/const.ts | 10 ++ .../screens/metrics/tableData.tsx | 1 - .../modelServing/screens/metrics/utils.tsx | 58 +++++++ ...ojectInferenceMetricsConfigurationPage.tsx | 5 +- .../src/pages/projects/ProjectViewRoutes.tsx | 2 + 22 files changed, 782 insertions(+), 90 deletions(-) create mode 100644 frontend/src/concepts/dashboard/DashboardModalFooter.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/BiasConfigurationAlertPopover.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/BiasConfigurationButton.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/MetricTypeField.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/useBiasConfigurationObject.ts create mode 100644 frontend/src/pages/modelServing/screens/metrics/const.ts diff --git a/frontend/src/api/prometheus/serving.ts b/frontend/src/api/prometheus/serving.ts index c868cd1527..057a700a87 100644 --- a/frontend/src/api/prometheus/serving.ts +++ b/frontend/src/api/prometheus/serving.ts @@ -1,5 +1,9 @@ import * as React from 'react'; -import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; +import { + ContextResourceData, + PrometheusQueryRangeResponseDataResult, + PrometheusQueryRangeResultValue, +} from '~/types'; import { InferenceMetricType, RuntimeMetricType, @@ -16,7 +20,10 @@ export const useModelServingMetrics = ( lastUpdateTime: number, setLastUpdateTime: (time: number) => void, ): { - data: Record>; + data: Record< + RuntimeMetricType | InferenceMetricType, + ContextResourceData + >; refresh: () => void; } => { const [end, setEnd] = React.useState(lastUpdateTime); @@ -88,6 +95,8 @@ export const useModelServingMetrics = ( runtimeMemoryUtilization, inferenceRequestSuccessCount, inferenceRequestFailedCount, + inferenceTrustyAIDIR, + inferenceTrustyAISPD, ]); const refreshAllMetrics = React.useCallback(() => { diff --git a/frontend/src/concepts/dashboard/DashboardModalFooter.tsx b/frontend/src/concepts/dashboard/DashboardModalFooter.tsx new file mode 100644 index 0000000000..a2019723ee --- /dev/null +++ b/frontend/src/concepts/dashboard/DashboardModalFooter.tsx @@ -0,0 +1,48 @@ +import { + ActionList, + ActionListItem, + Alert, + Button, + Stack, + StackItem, +} from '@patternfly/react-core'; +import * as React from 'react'; + +type DashboardModalFooterProps = { + submitLabel: string; + onSubmit: () => void; + onCancel: () => void; + isSubmitDisabled: boolean; + alertTitle: string; + error?: Error; +}; + +const DashboardModalFooter: React.FC = ( + { submitLabel, onSubmit, onCancel, isSubmitDisabled, error, alertTitle }, // make sure alert uses the full width +) => ( + + {error && ( + + + {error.message} + + + )} + + + + + + + + + + + +); + +export default DashboardModalFooter; diff --git a/frontend/src/pages/modelServing/ModelServingRoutes.tsx b/frontend/src/pages/modelServing/ModelServingRoutes.tsx index ce09c5b00d..2d99bf81a9 100644 --- a/frontend/src/pages/modelServing/ModelServingRoutes.tsx +++ b/frontend/src/pages/modelServing/ModelServingRoutes.tsx @@ -19,6 +19,7 @@ const ModelServingRoutes: React.FC = () => { } /> {modelMetricsEnabled && ( }> + } /> }> } /> } /> @@ -27,6 +28,7 @@ const ModelServingRoutes: React.FC = () => { } /> )} + } /> ); diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationAlertPopover.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationAlertPopover.tsx new file mode 100644 index 0000000000..7d2708ef99 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationAlertPopover.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { Button, Icon, Popover } from '@patternfly/react-core'; +import { InfoCircleIcon } from '@patternfly/react-icons'; +import { EMPTY_BIAS_CONFIGURATION_DESC, EMPTY_BIAS_CONFIGURATION_TITLE } from './const'; + +type BiasConfigurationAlertPopoverProps = { + onConfigure: () => void; +}; + +const BiasConfigurationAlertPopover: React.FC = ({ + onConfigure, +}) => ( + } + bodyContent={EMPTY_BIAS_CONFIGURATION_DESC} + footerContent={ + + } + > + + + + +); + +export default BiasConfigurationAlertPopover; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationBreadcrumbPage.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationBreadcrumbPage.tsx index bbbfa8b54d..bf646c566c 100644 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationBreadcrumbPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationBreadcrumbPage.tsx @@ -18,7 +18,7 @@ const BiasConfigurationBreadcrumbPage: React.FC = () => { }, { label: 'Metric configuration', isActive: true }, ]} - modelDisplayName={modelDisplayName} + inferenceService={inferenceService} /> ); }; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationButton.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationButton.tsx new file mode 100644 index 0000000000..572b6ed45a --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationButton.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { Button } from '@patternfly/react-core'; +import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; +import { InferenceServiceKind } from '~/k8sTypes'; +import ManageBiasConfigurationModal from './biasConfigurationModal/ManageBiasConfigurationModal'; + +type BiasConfigurationButtonProps = { + inferenceService: InferenceServiceKind; +}; + +const BiasConfigurationButton: React.FC = ({ inferenceService }) => { + const [isOpen, setOpen] = React.useState(false); + const { biasMetricConfigs, loaded, refresh } = useExplainabilityModelData(); + + React.useEffect(() => { + if (loaded && biasMetricConfigs.length === 0) { + setOpen(true); + } + }, [loaded, biasMetricConfigs]); + + return ( + <> + + { + if (submit) { + refresh(); + } + setOpen(false); + }} + inferenceService={inferenceService} + /> + + ); +}; + +export default BiasConfigurationButton; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx index fabf4c8eae..79b46a87e4 100644 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx @@ -4,21 +4,22 @@ import { useNavigate } from 'react-router-dom'; import ApplicationsPage from '~/pages/ApplicationsPage'; import { BreadcrumbItemType } from '~/types'; import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; +import { InferenceServiceKind } from '~/k8sTypes'; +import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; import { MetricsTabKeys } from './types'; import BiasConfigurationTable from './BiasConfigurationTable'; import { getBreadcrumbItemComponents } from './utils'; type BiasConfigurationPageProps = { breadcrumbItems: BreadcrumbItemType[]; - modelDisplayName: string; + inferenceService: InferenceServiceKind; }; const BiasConfigurationPage: React.FC = ({ breadcrumbItems, - modelDisplayName, + inferenceService, }) => { const { biasMetricConfigs, loaded } = useExplainabilityModelData(); - const emptyConfiguration = biasMetricConfigs.length === 0; const navigate = useNavigate(); return ( = ({ breadcrumb={{getBreadcrumbItemComponents(breadcrumbItems)}} headerAction={ } loaded={loaded} @@ -35,7 +38,7 @@ const BiasConfigurationPage: React.FC = ({ // The page is not empty, we will handle the empty state in the table empty={false} > - + ); }; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx index 4eaa6b4c78..78a848eefb 100644 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx @@ -3,26 +3,36 @@ import { Button, ToolbarItem } from '@patternfly/react-core'; import Table from '~/components/table/Table'; import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField'; import { BiasMetricConfig } from '~/concepts/explainability/types'; +import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; +import { InferenceServiceKind } from '~/k8sTypes'; +import DeleteBiasConfigurationModal from '~/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal'; +import ManageBiasConfigurationModal from './biasConfigurationModal/ManageBiasConfigurationModal'; import BiasConfigurationTableRow from './BiasConfigurationTableRow'; import { columns } from './tableData'; import BiasConfigurationEmptyState from './BiasConfigurationEmptyState'; +import BiasConfigurationButton from './BiasConfigurationButton'; type BiasConfigurationTableProps = { - configurations: BiasMetricConfig[]; + inferenceService: InferenceServiceKind; }; -const BiasConfigurationTable: React.FC = ({ configurations }) => { +const BiasConfigurationTable: React.FC = ({ inferenceService }) => { + const { biasMetricConfigs, refresh } = useExplainabilityModelData(); const [searchType, setSearchType] = React.useState(SearchType.NAME); const [search, setSearch] = React.useState(''); - const filteredConfigurations = configurations.filter((configuration) => { + const [cloneConfiguration, setCloneConfiguration] = React.useState(); + const [deleteConfiguration, setDeleteConfiguration] = React.useState(); + + const filteredConfigurations = biasMetricConfigs.filter((configuration) => { if (!search) { return true; } - // TODO: add more search types switch (searchType) { case SearchType.NAME: return configuration.name.toLowerCase().includes(search.toLowerCase()); + case SearchType.METRIC: + return configuration.metricType.toLowerCase().includes(search.toLocaleLowerCase()); case SearchType.PROTECTED_ATTRIBUTE: return configuration.protectedAttribute.toLowerCase().includes(search.toLowerCase()); case SearchType.OUTPUT: @@ -43,53 +53,82 @@ const BiasConfigurationTable: React.FC = ({ configu Object.keys(SearchType).filter( (key) => SearchType[key] === SearchType.NAME || + SearchType[key] === SearchType.METRIC || SearchType[key] === SearchType.PROTECTED_ATTRIBUTE || SearchType[key] === SearchType.OUTPUT, ), [], ); return ( -
setExpanded(!isExpanded), + }} + /> + {obj.name}{obj.metricType}{obj.protectedAttribute}{obj.privilegedAttribute}{obj.unprivilegedAttribute}{obj.outcomeName}{obj.favorableOutcome} + {/* TODO: add actions */} + +
+ + + + + Violation threshold + {obj.thresholdDelta} + + + Metric batch size + {obj.batchSize} + + + +
( - - )} - emptyTableView={ - search ? ( + <> +
( + + )} + emptyTableView={ + search ? ( + <> + No metric configurations match your filters.{' '} + + + ) : ( + + ) + } + toolbarContent={ <> - No metric configurations match your filters.{' '} - + + { + setSearchType(searchType); + }} + onSearchValueChange={(searchValue) => { + setSearch(searchValue); + }} + /> + + + + - ) : ( - - ) - } - toolbarContent={ - <> - - { - setSearchType(searchType); - }} - onSearchValueChange={(searchValue) => { - setSearch(searchValue); - }} - /> - - - {/* TODO: add configure metric action */} - - - - } - /> + } + /> + { + if (submit) { + refresh(); + } + setCloneConfiguration(undefined); + }} + inferenceService={inferenceService} + /> + { + if (deleted) { + refresh(); + } + setDeleteConfiguration(undefined); + }} + /> + ); }; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx index d9a98a4a52..9161f142d5 100644 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx @@ -11,9 +11,16 @@ import { BiasMetricConfig } from '~/concepts/explainability/types'; type BiasConfigurationTableRowProps = { obj: BiasMetricConfig; rowIndex: number; + onCloneConfiguration: (obj: BiasMetricConfig) => void; + onDeleteConfiguration: (obj: BiasMetricConfig) => void; }; -const BiasConfigurationTableRow: React.FC = ({ obj, rowIndex }) => { +const BiasConfigurationTableRow: React.FC = ({ + obj, + rowIndex, + onCloneConfiguration, + onDeleteConfiguration, +}) => { const [isExpanded, setExpanded] = React.useState(false); return ( @@ -35,8 +42,22 @@ const BiasConfigurationTableRow: React.FC = ({ o diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx index b7a335c902..9047225528 100644 --- a/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx @@ -1,25 +1,38 @@ import React from 'react'; import { PageSection, Stack, StackItem } from '@patternfly/react-core'; -import DIRGraph from '~/pages/modelServing/screens/metrics/DIRChart'; +import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; +import DIRGraph from './DIRChart'; import MetricsPageToolbar from './MetricsPageToolbar'; import SPDChart from './SPDChart'; +import EmptyBiasConfigurationCard from './EmptyBiasConfigurationCard'; -const BiasTab = () => ( - - - - - - - - - - - - - - - -); +const BiasTab = () => { + const { biasMetricConfigs } = useExplainabilityModelData(); + return ( + + + + + + + {biasMetricConfigs.length === 0 ? ( + + + + ) : ( + <> + + + + + + + + )} + + + + ); +}; export default BiasTab; diff --git a/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx b/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx new file mode 100644 index 0000000000..05d6d5d9ef --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { + Button, + Card, + CardBody, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + Title, +} from '@patternfly/react-core'; +import { WrenchIcon } from '@patternfly/react-icons'; +import { useNavigate } from 'react-router-dom'; +import { EMPTY_BIAS_CONFIGURATION_DESC, EMPTY_BIAS_CONFIGURATION_TITLE } from './const'; + +const EmptyBiasConfigurationCard: React.FC = () => { + const navigate = useNavigate(); + return ( + + + + + + {EMPTY_BIAS_CONFIGURATION_TITLE} + + {EMPTY_BIAS_CONFIGURATION_DESC} + + + + + ); +}; + +export default EmptyBiasConfigurationCard; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx index a9ebb1f8cf..5df7897f26 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx @@ -1,8 +1,11 @@ import * as React from 'react'; -import { Breadcrumb } from '@patternfly/react-core'; +import { Breadcrumb, Button } from '@patternfly/react-core'; +import { useNavigate, useParams } from 'react-router-dom'; +import { CogIcon } from '@patternfly/react-icons'; import { BreadcrumbItemType } from '~/types'; import ApplicationsPage from '~/pages/ApplicationsPage'; import MetricsPageTabs from '~/pages/modelServing/screens/metrics/MetricsPageTabs'; +import { MetricsTabKeys } from '~/pages/modelServing/screens/metrics/types'; import { getBreadcrumbItemComponents } from './utils'; type MetricsPageProps = { @@ -10,16 +13,32 @@ type MetricsPageProps = { breadcrumbItems: BreadcrumbItemType[]; }; -const MetricsPage: React.FC = ({ title, breadcrumbItems }) => ( - {getBreadcrumbItemComponents(breadcrumbItems)}} - loaded - description={null} - empty={false} - > - - -); +const MetricsPage: React.FC = ({ title, breadcrumbItems }) => { + const { tab } = useParams(); + const navigate = useNavigate(); + return ( + {getBreadcrumbItemComponents(breadcrumbItems)}} + // TODO: decide whether we need to set the loaded based on the feature flag and explainability loaded + loaded + description={null} + empty={false} + headerAction={ + tab === MetricsTabKeys.BIAS && ( + + ) + } + > + + + ); +}; export default MetricsPage; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx index df7ccce86a..34fcf5925e 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx @@ -1,20 +1,26 @@ import React from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Tabs, Tab, TabTitleText } from '@patternfly/react-core'; +import { Tabs, Tab, TabTitleText, TabAction } from '@patternfly/react-core'; import { MetricsTabKeys } from '~/pages/modelServing/screens/metrics/types'; +import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; import PerformanceTab from './PerformanceTab'; import BiasTab from './BiasTab'; +import BiasConfigurationAlertPopover from './BiasConfigurationAlertPopover'; + import './MetricsPageTabs.scss'; const MetricsPageTabs: React.FC = () => { const DEFAULT_TAB = MetricsTabKeys.PERFORMANCE; + const { biasMetricConfigs, loaded } = useExplainabilityModelData(); - const { tab } = useParams(); + const { tab } = useParams<{ tab: MetricsTabKeys }>(); const navigate = useNavigate(); React.useEffect(() => { if (!tab) { navigate(`./${DEFAULT_TAB}`, { replace: true }); + } else if (!Object.values(MetricsTabKeys).includes(tab)) { + navigate(`../${DEFAULT_TAB}`, { replace: true }); } }, [DEFAULT_TAB, navigate, tab]); @@ -44,6 +50,18 @@ const MetricsPageTabs: React.FC = () => { title={Bias} aria-label="Bias tab" className="odh-tabcontent-fix" + actions={ + loaded && + biasMetricConfigs.length === 0 && ( + + { + navigate('../configure', { relative: 'path' }); + }} + /> + + ) + } > diff --git a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal.tsx b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal.tsx new file mode 100644 index 0000000000..d3f19f4d0d --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { MetricTypes } from '~/api'; +import { ExplainabilityContext } from '~/concepts/explainability/ExplainabilityContext'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; +import DeleteModal from '~/pages/projects/components/DeleteModal'; + +type DeleteBiasConfigurationModalProps = { + configurationToDelete?: BiasMetricConfig; + onClose: (deleted: boolean) => void; +}; + +const DeleteBiasConfigurationModal: React.FC = ({ + onClose, + configurationToDelete, +}) => { + const [isDeleting, setDeleting] = React.useState(false); + const [error, setError] = React.useState(); + const { + apiState: { api }, + } = React.useContext(ExplainabilityContext); + + const onBeforeClose = (deleted: boolean) => { + onClose(deleted); + setDeleting(false); + setError(undefined); + }; + + const displayName = configurationToDelete ? configurationToDelete.name : 'this bias metric'; + return ( + onBeforeClose(false)} + submitButtonLabel="Delete bias metric" + onDelete={() => { + if (configurationToDelete) { + setDeleting(true); + const deleteFunc = + configurationToDelete.metricType === MetricTypes.DIR + ? api.deleteDirRequest + : api.deleteSpdRequest; + deleteFunc({}, configurationToDelete.id) + .then(() => onBeforeClose(true)) + .catch((e) => { + setError(e); + setDeleting(false); + }); + } + }} + deleting={isDeleting} + error={error} + deleteName={displayName} + > + This action cannot be undone. + + ); +}; + +export default DeleteBiasConfigurationModal; diff --git a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx new file mode 100644 index 0000000000..c43dcc7896 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; +import { Form, FormGroup, Modal, TextInput, Tooltip } from '@patternfly/react-core'; +import { HelpIcon } from '@patternfly/react-icons'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; +import { checkConfigurationFieldsValid } from '~/pages/modelServing/screens/metrics/utils'; +import { MetricTypes } from '~/api'; +import { InferenceServiceKind } from '~/k8sTypes'; +import { ExplainabilityContext } from '~/concepts/explainability/ExplainabilityContext'; +import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; +import useBiasConfigurationObject from './useBiasConfigurationObject'; +import MetricTypeField from './MetricTypeField'; + +type ManageBiasConfigurationModalProps = { + existingConfiguration?: BiasMetricConfig; + isOpen: boolean; + onClose: (submit: boolean) => void; + inferenceService: InferenceServiceKind; +}; + +const ManageBiasConfigurationModal: React.FC = ({ + existingConfiguration, + isOpen, + onClose, + inferenceService, +}) => { + const { + apiState: { api }, + } = React.useContext(ExplainabilityContext); + const [configuration, setConfiguration] = useBiasConfigurationObject( + inferenceService.metadata.name, + existingConfiguration, + ); + const [actionInProgress, setActionInProgress] = React.useState(false); + const [error, setError] = React.useState(); + const [metricType, setMetricType] = React.useState(); + + React.useEffect(() => { + setMetricType(existingConfiguration?.metricType); + }, [existingConfiguration]); + + const onBeforeClose = (submitted: boolean) => { + onClose(submitted); + setError(undefined); + setActionInProgress(false); + }; + + const onCreateConfiguration = () => { + const createFunc = metricType === MetricTypes.SPD ? api.createSpdRequest : api.createDirRequest; + setActionInProgress(true); + createFunc({}, configuration) + .then(() => onBeforeClose(true)) + .catch((e) => { + setError(e); + setActionInProgress(false); + }); + }; + + return ( + onBeforeClose(false)} + footer={ + onBeforeClose(false)} + onSubmit={onCreateConfiguration} + submitLabel="Configure" + alertTitle="Failed to configure bias metric" + isSubmitDisabled={ + !checkConfigurationFieldsValid(configuration, metricType) || actionInProgress + } + /> + } + description="All fields are required." + > +
+ + setConfiguration('requestName', value)} + /> + + + + setConfiguration('protectedAttribute', value)} + /> + + + setConfiguration('privilegedAttribute', value)} + /> + + + setConfiguration('unprivilegedAttribute', value)} + /> + + + setConfiguration('outcomeName', value)} + /> + + + setConfiguration('favorableOutcome', value)} + /> + + + + + } + > + setConfiguration('thresholdDelta', Number(value))} + /> + + + + + } + > + setConfiguration('batchSize', Number(value))} + /> + + +
+ ); +}; + +export default ManageBiasConfigurationModal; diff --git a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/MetricTypeField.tsx b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/MetricTypeField.tsx new file mode 100644 index 0000000000..3c644ffc10 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/MetricTypeField.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { FormGroup, Select, SelectOption, Tooltip } from '@patternfly/react-core'; +import { HelpIcon } from '@patternfly/react-icons'; +import { METRIC_TYPE_DISPLAY_NAME } from '~/pages/modelServing/screens/metrics/const'; +import { MetricTypes } from '~/api'; +import { isMetricType } from '~/pages/modelServing/screens/metrics/utils'; + +type MetricTypeFieldProps = { + fieldId: string; + value?: MetricTypes; + setValue: (value: MetricTypes) => void; +}; + +const MetricTypeField: React.FC = ({ fieldId, value, setValue }) => { + const [isOpen, setOpen] = React.useState(false); + return ( + // TODO: decide what to show in the helper tooltip + + + + } + > + + + ); +}; + +export default MetricTypeField; diff --git a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/useBiasConfigurationObject.ts b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/useBiasConfigurationObject.ts new file mode 100644 index 0000000000..eb531a72fe --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/useBiasConfigurationObject.ts @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; +import useGenericObjectState from '~/utilities/useGenericObjectState'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; +import { BaseMetricRequest } from '~/api'; + +const useBiasConfigurationObject = ( + modelId: string, + existingData?: BiasMetricConfig, +): [ + data: BaseMetricRequest, + setData: UpdateObjectAtPropAndValue, + resetDefaults: () => void, +] => { + const createConfiguration = useGenericObjectState({ + modelId: modelId, + requestName: '', + protectedAttribute: '', + privilegedAttribute: '', + unprivilegedAttribute: '', + outcomeName: '', + favorableOutcome: '', + thresholdDelta: 0.1, + batchSize: 5000, + }); + + const [, setCreateData] = createConfiguration; + + const existingModelId = existingData?.modelId ?? modelId; + const existingName = existingData?.name ?? ''; + const existingProtectedAttribute = existingData?.protectedAttribute ?? ''; + const existingPrivilegedAttribute = existingData?.privilegedAttribute ?? ''; + const existingUnprivilegedAttribute = existingData?.unprivilegedAttribute ?? ''; + const existingOutcomeName = existingData?.outcomeName ?? ''; + const existingFavorableOutcome = existingData?.favorableOutcome ?? ''; + const existingThresholdDelta = existingData?.thresholdDelta ?? 0.1; + const existingBatchSize = existingData?.batchSize ?? 5000; + + React.useEffect(() => { + if (existingData) { + setCreateData('modelId', existingModelId); + setCreateData('requestName', ''); + setCreateData('protectedAttribute', existingProtectedAttribute); + setCreateData('privilegedAttribute', existingPrivilegedAttribute); + setCreateData('unprivilegedAttribute', existingUnprivilegedAttribute); + setCreateData('outcomeName', existingOutcomeName); + setCreateData('favorableOutcome', existingFavorableOutcome); + setCreateData('thresholdDelta', existingThresholdDelta); + setCreateData('batchSize', existingBatchSize); + } + }, [ + setCreateData, + existingData, + existingModelId, + existingName, + existingProtectedAttribute, + existingPrivilegedAttribute, + existingUnprivilegedAttribute, + existingOutcomeName, + existingFavorableOutcome, + existingThresholdDelta, + existingBatchSize, + ]); + + return createConfiguration; +}; + +export default useBiasConfigurationObject; diff --git a/frontend/src/pages/modelServing/screens/metrics/const.ts b/frontend/src/pages/modelServing/screens/metrics/const.ts new file mode 100644 index 0000000000..865382a41d --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/const.ts @@ -0,0 +1,10 @@ +import { MetricTypes } from '~/api'; + +export const EMPTY_BIAS_CONFIGURATION_TITLE = 'Bias metrics not configured'; +export const EMPTY_BIAS_CONFIGURATION_DESC = + 'Bias metrics for this model have not been configured. To monitor model bias, you must first configure metrics.'; + +export const METRIC_TYPE_DISPLAY_NAME = { + [MetricTypes.DIR]: 'Disparate impact ratio (DIR)', + [MetricTypes.SPD]: 'Statistical parity difference (SPD)', +}; diff --git a/frontend/src/pages/modelServing/screens/metrics/tableData.tsx b/frontend/src/pages/modelServing/screens/metrics/tableData.tsx index 8b93b5a5cb..2c44a6d0a4 100644 --- a/frontend/src/pages/modelServing/screens/metrics/tableData.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/tableData.tsx @@ -1,7 +1,6 @@ import { SortableData } from '~/components/table/useTableColumnSort'; import { BiasMetricConfig } from '~/concepts/explainability/types'; -// TODO: add sortable export const columns: SortableData[] = [ { field: 'expand', diff --git a/frontend/src/pages/modelServing/screens/metrics/utils.tsx b/frontend/src/pages/modelServing/screens/metrics/utils.tsx index 26878160da..a4fcc6ace2 100644 --- a/frontend/src/pages/modelServing/screens/metrics/utils.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/utils.tsx @@ -13,6 +13,7 @@ import { NamedMetricChartLine, TranslatePoint, } from '~/pages/modelServing/screens/metrics/types'; +import { BaseMetricRequest, MetricTypes } from '~/api'; import { InferenceMetricType, RuntimeMetricType } from './ModelServingMetricsContext'; export const isModelMetricsEnabled = ( @@ -163,3 +164,60 @@ export const getBreadcrumbItemComponents = (breadcrumbItems: BreadcrumbItemType[ render={() => (item.link ? {item.label} : <>{item.label})} /> )); + +const checkThresholdValid = (metricType: MetricTypes, thresholdDelta?: number): boolean => { + if (thresholdDelta) { + if (metricType === MetricTypes.SPD) { + if (thresholdDelta > 0 && thresholdDelta < 1) { + // SPD, 0 < threshold < 1, valid + return true; + } + // SPD, not within the range, invalid + return false; + } + // DIR, no limitation, valid + if (metricType === MetricTypes.DIR) { + return true; + } + // not SPD not DIR, undefined for now, metricType should be selected, invalid + return false; + } + // not input anything, invalid + return false; +}; + +const checkBatchSizeValid = (batchSize?: number): boolean => { + if (batchSize) { + if (Number.isInteger(batchSize)) { + // size > 2, integer, valid + if (batchSize > 2) { + return true; + } + // size <= 2, invalid + return false; + } + // not an integer, invalid + return false; + } + // not input anything, invalid + return false; +}; + +export const checkConfigurationFieldsValid = ( + configurations: BaseMetricRequest, + metricType?: MetricTypes, +) => + metricType !== undefined && + configurations.requestName !== '' && + configurations.protectedAttribute !== '' && + configurations.privilegedAttribute !== '' && + configurations.unprivilegedAttribute !== '' && + configurations.outcomeName !== '' && + configurations.favorableOutcome !== '' && + configurations.batchSize !== undefined && + configurations.batchSize > 0 && + checkThresholdValid(metricType, configurations.thresholdDelta) && + checkBatchSizeValid(configurations.batchSize); + +export const isMetricType = (metricType: string | SelectOptionObject): metricType is MetricTypes => + Object.values(MetricTypes).includes(metricType as MetricTypes); diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsConfigurationPage.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsConfigurationPage.tsx index f631463176..bca3442359 100644 --- a/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsConfigurationPage.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceMetricsConfigurationPage.tsx @@ -8,7 +8,6 @@ import { ProjectInferenceMetricsOutletContextProps } from './ProjectInferenceMet const ProjectInferenceMetricsConfigurationPage: React.FC = () => { const { currentProject, inferenceService } = useOutletContext(); - const modelDisplayName = getInferenceServiceDisplayName(inferenceService); return ( { link: `/projects/${currentProject.metadata.name}`, }, { - label: modelDisplayName, + label: getInferenceServiceDisplayName(inferenceService), link: `/projects/${currentProject.metadata.name}/metrics/model/${inferenceService.metadata.name}`, }, { label: 'Metric configuration', isActive: true }, ]} - modelDisplayName={modelDisplayName} + inferenceService={inferenceService} /> ); }; diff --git a/frontend/src/pages/projects/ProjectViewRoutes.tsx b/frontend/src/pages/projects/ProjectViewRoutes.tsx index 9d4e96370c..6a3cb12620 100644 --- a/frontend/src/pages/projects/ProjectViewRoutes.tsx +++ b/frontend/src/pages/projects/ProjectViewRoutes.tsx @@ -31,10 +31,12 @@ const ProjectViewRoutes: React.FC = () => { {modelMetricsEnabled && ( <> }> + } /> }> } /> } /> + } /> } /> From 694ada2446c53e48364f6c0f8e7020af79164435 Mon Sep 17 00:00:00 2001 From: Alex Creasy Date: Mon, 12 Jun 2023 18:51:07 +0100 Subject: [PATCH 08/17] Multi-metric display on model bias screen (#1273) (#1349) * Enhancements to model bias screen * Display of multiple bias charts simultaneously * Multi-select component, allowing free text, or select-from-list selection of chartst to display * Ability to collapse / expand individual charts * User selectable refresh rates of chart data * Chart selection and open / closed status is persisted to session cache for life of user's browser session Display user defined threshold values on charts (#1163) * Clean up of bias chart logic * Displays thresholds chosen by user, or defaults if none. * Improves domain and threshold calculation based on user values or defaults --- frontend/src/api/prometheus/serving.ts | 15 ++- .../prometheus/useQueryRangeResourceData.ts | 14 ++- .../dashboard/DashboardExpandableSection.scss | 9 ++ .../dashboard/DashboardExpandableSection.tsx | 32 +++++ .../src/pages/modelServing/screens/const.ts | 14 ++- .../metrics/BiasMetricConfigSelector.tsx | 90 +++++++++++++++ .../modelServing/screens/metrics/BiasTab.tsx | 95 ++++++++++++--- .../modelServing/screens/metrics/DIRChart.tsx | 42 ------- .../metrics/EmptyBiasChartSelectionCard.tsx | 30 +++++ .../screens/metrics/InferenceGraphs.tsx | 6 - .../screens/metrics/MetricsPage.tsx | 1 + .../screens/metrics/MetricsPageTabs.tsx | 4 +- .../screens/metrics/MetricsPageToolbar.tsx | 109 ++++++++++++------ .../metrics/ModelServingMetricsContext.tsx | 17 ++- .../modelServing/screens/metrics/SPDChart.tsx | 39 ------- .../screens/metrics/ScheduledMetricSelect.tsx | 34 ------ .../screens/metrics/TrustyChart.tsx | 94 ++++----------- .../modelServing/screens/metrics/const.ts | 63 +++++++++- .../modelServing/screens/metrics/types.ts | 23 +++- .../modelServing/screens/metrics/utils.tsx | 68 ++++++++++- .../src/pages/modelServing/screens/types.ts | 9 ++ 21 files changed, 547 insertions(+), 261 deletions(-) create mode 100644 frontend/src/concepts/dashboard/DashboardExpandableSection.scss create mode 100644 frontend/src/concepts/dashboard/DashboardExpandableSection.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/BiasMetricConfigSelector.tsx delete mode 100644 frontend/src/pages/modelServing/screens/metrics/DIRChart.tsx create mode 100644 frontend/src/pages/modelServing/screens/metrics/EmptyBiasChartSelectionCard.tsx delete mode 100644 frontend/src/pages/modelServing/screens/metrics/SPDChart.tsx delete mode 100644 frontend/src/pages/modelServing/screens/metrics/ScheduledMetricSelect.tsx diff --git a/frontend/src/api/prometheus/serving.ts b/frontend/src/api/prometheus/serving.ts index 057a700a87..c2dd132a2d 100644 --- a/frontend/src/api/prometheus/serving.ts +++ b/frontend/src/api/prometheus/serving.ts @@ -8,7 +8,11 @@ import { InferenceMetricType, RuntimeMetricType, } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { MetricType, TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { + MetricType, + RefreshIntervalTitle, + TimeframeTitle, +} from '~/pages/modelServing/screens/types'; import useQueryRangeResourceData, { useQueryRangeResourceDataTrusty, } from './useQueryRangeResourceData'; @@ -19,6 +23,7 @@ export const useModelServingMetrics = ( timeframe: TimeframeTitle, lastUpdateTime: number, setLastUpdateTime: (time: number) => void, + refreshInterval: RefreshIntervalTitle, ): { data: Record< RuntimeMetricType | InferenceMetricType, @@ -33,6 +38,7 @@ export const useModelServingMetrics = ( queries[RuntimeMetricType.REQUEST_COUNT], end, timeframe, + refreshInterval, ); const runtimeAverageResponseTime = useQueryRangeResourceData( @@ -40,6 +46,7 @@ export const useModelServingMetrics = ( queries[RuntimeMetricType.AVG_RESPONSE_TIME], end, timeframe, + refreshInterval, ); const runtimeCPUUtilization = useQueryRangeResourceData( @@ -47,6 +54,7 @@ export const useModelServingMetrics = ( queries[RuntimeMetricType.CPU_UTILIZATION], end, timeframe, + refreshInterval, ); const runtimeMemoryUtilization = useQueryRangeResourceData( @@ -54,6 +62,7 @@ export const useModelServingMetrics = ( queries[RuntimeMetricType.MEMORY_UTILIZATION], end, timeframe, + refreshInterval, ); const inferenceRequestSuccessCount = useQueryRangeResourceData( @@ -61,6 +70,7 @@ export const useModelServingMetrics = ( queries[InferenceMetricType.REQUEST_COUNT_SUCCESS], end, timeframe, + refreshInterval, ); const inferenceRequestFailedCount = useQueryRangeResourceData( @@ -68,6 +78,7 @@ export const useModelServingMetrics = ( queries[InferenceMetricType.REQUEST_COUNT_FAILED], end, timeframe, + refreshInterval, ); const inferenceTrustyAISPD = useQueryRangeResourceDataTrusty( @@ -75,6 +86,7 @@ export const useModelServingMetrics = ( queries[InferenceMetricType.TRUSTY_AI_SPD], end, timeframe, + refreshInterval, ); const inferenceTrustyAIDIR = useQueryRangeResourceDataTrusty( @@ -82,6 +94,7 @@ export const useModelServingMetrics = ( queries[InferenceMetricType.TRUSTY_AI_DIR], end, timeframe, + refreshInterval, ); React.useEffect(() => { diff --git a/frontend/src/api/prometheus/useQueryRangeResourceData.ts b/frontend/src/api/prometheus/useQueryRangeResourceData.ts index d04955af67..d9a07be34e 100644 --- a/frontend/src/api/prometheus/useQueryRangeResourceData.ts +++ b/frontend/src/api/prometheus/useQueryRangeResourceData.ts @@ -1,6 +1,10 @@ import * as React from 'react'; -import { TimeframeStep, TimeframeTimeRange } from '~/pages/modelServing/screens/const'; -import { TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { + RefreshIntervalValue, + TimeframeStep, + TimeframeTimeRange, +} from '~/pages/modelServing/screens/const'; +import { RefreshIntervalTitle, TimeframeTitle } from '~/pages/modelServing/screens/types'; import { ContextResourceData, PrometheusQueryRangeResponseDataResult, @@ -15,6 +19,7 @@ const useQueryRangeResourceData = ( query: string, end: number, timeframe: TimeframeTitle, + refreshInterval: RefreshIntervalTitle, ): ContextResourceData => { const responsePredicate = React.useCallback( (data) => data.result?.[0]?.values || [], @@ -30,7 +35,7 @@ const useQueryRangeResourceData = ( TimeframeStep[timeframe], responsePredicate, ), - 5 * 60 * 1000, + RefreshIntervalValue[refreshInterval], ); }; @@ -42,6 +47,7 @@ export const useQueryRangeResourceDataTrusty = ( query: string, end: number, timeframe: TimeframeTitle, + refreshInterval: RefreshIntervalTitle, ): ContextResourceData => { const responsePredicate = React.useCallback>( (data) => data.result, @@ -57,7 +63,7 @@ export const useQueryRangeResourceDataTrusty = ( TimeframeStep[timeframe], responsePredicate, ), - 5 * 60 * 1000, + RefreshIntervalValue[refreshInterval], ); }; export default useQueryRangeResourceData; diff --git a/frontend/src/concepts/dashboard/DashboardExpandableSection.scss b/frontend/src/concepts/dashboard/DashboardExpandableSection.scss new file mode 100644 index 0000000000..7c45303688 --- /dev/null +++ b/frontend/src/concepts/dashboard/DashboardExpandableSection.scss @@ -0,0 +1,9 @@ +.dashboard-expandable-section-heading > .pf-c-expandable-section__toggle > * { + font-family: var(--pf-global--FontFamily--sans-serif) !important; + font-size: var(--pf-global--FontSize--2xl) !important; + color: var(--pf-global--icon--Color--light) !important; +} + +.dashboard-expandable-section-heading > .pf-c-expandable-section__toggle:hover > * { + color: var(--pf-global--icon--Color--dark) !important; +} diff --git a/frontend/src/concepts/dashboard/DashboardExpandableSection.tsx b/frontend/src/concepts/dashboard/DashboardExpandableSection.tsx new file mode 100644 index 0000000000..c40cb38326 --- /dev/null +++ b/frontend/src/concepts/dashboard/DashboardExpandableSection.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { ExpandableSection } from '@patternfly/react-core'; +import { useBrowserStorage } from '~/components/browserStorage'; + +import './DashboardExpandableSection.scss'; + +type DashboardExpandableSectionProps = { + children: React.ReactNode; + title: string; + storageKey: string; +}; + +const DashboardExpandableSection: React.FC = ({ + children, + title, + storageKey, +}) => { + const [isExpanded, setIsExpanded] = useBrowserStorage(storageKey, true, true, true); + + return ( + + {children} + + ); +}; + +export default DashboardExpandableSection; diff --git a/frontend/src/pages/modelServing/screens/const.ts b/frontend/src/pages/modelServing/screens/const.ts index 905c4ccd15..7c30346dd5 100644 --- a/frontend/src/pages/modelServing/screens/const.ts +++ b/frontend/src/pages/modelServing/screens/const.ts @@ -1,4 +1,11 @@ -import { ServingRuntimeSize, TimeframeStepType, TimeframeTimeType, TimeframeTitle } from './types'; +import { + RefreshIntervalTitle, + RefreshIntervalValueType, + ServingRuntimeSize, + TimeframeStepType, + TimeframeTimeType, + TimeframeTitle, +} from './types'; export const DEFAULT_MODEL_SERVER_SIZES: ServingRuntimeSize[] = [ { @@ -84,3 +91,8 @@ export const TimeframeStep: TimeframeStepType = { [TimeframeTitle.ONE_MONTH]: 30 * 7 * 24 * 12, // [TimeframeTitle.UNLIMITED]: 30 * 7 * 24 * 12, // TODO: determine if we "zoom out" more }; + +export const RefreshIntervalValue: RefreshIntervalValueType = { + [RefreshIntervalTitle.ONE_MINUTE]: 60 * 1000, + [RefreshIntervalTitle.FIVE_MINUTES]: 5 * 60 * 1000, +}; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasMetricConfigSelector.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasMetricConfigSelector.tsx new file mode 100644 index 0000000000..a3bb9bf42c --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/BiasMetricConfigSelector.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { Select, SelectGroup, SelectOption, SelectVariant } from '@patternfly/react-core'; +import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; +import { MetricTypes } from '~/api'; +import { + byId, + byNotId, + createBiasSelectOption, + isBiasSelectOption, +} from '~/pages/modelServing/screens/metrics/utils'; +import { BiasSelectOption } from '~/pages/modelServing/screens/metrics/types'; + +type BiasMetricConfigSelectorProps = { + onChange: (x: BiasMetricConfig[]) => void; + initialSelections: BiasMetricConfig[]; +}; + +const BiasMetricConfigSelector: React.FC = ({ + onChange, + initialSelections, +}) => { + const { biasMetricConfigs, loaded } = useExplainabilityModelData(); + + const [isOpen, setIsOpen] = React.useState(false); + const [selected, setSelected] = React.useState( + initialSelections.map(createBiasSelectOption), + ); + + const elementId = React.useId(); + + const changeState = React.useCallback( + (options: BiasSelectOption[]) => { + setSelected(options); + onChange(options.map((x) => x.biasMetricConfig)); + }, + [onChange], + ); + + return ( +
+ + +
+ ); +}; + +export default BiasMetricConfigSelector; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx index 9047225528..b95bf5596b 100644 --- a/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx @@ -1,38 +1,97 @@ import React from 'react'; -import { PageSection, Stack, StackItem } from '@patternfly/react-core'; +import { + Bullseye, + PageSection, + Spinner, + Stack, + StackItem, + ToolbarGroup, + ToolbarItem, +} from '@patternfly/react-core'; +import { useParams } from 'react-router-dom'; +import MetricsPageToolbar from '~/pages/modelServing/screens/metrics/MetricsPageToolbar'; +import BiasMetricConfigSelector from '~/pages/modelServing/screens/metrics/BiasMetricConfigSelector'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; -import DIRGraph from './DIRChart'; -import MetricsPageToolbar from './MetricsPageToolbar'; -import SPDChart from './SPDChart'; -import EmptyBiasConfigurationCard from './EmptyBiasConfigurationCard'; +import TrustyChart from '~/pages/modelServing/screens/metrics/TrustyChart'; +import { useBrowserStorage } from '~/components/browserStorage'; +import EmptyBiasConfigurationCard from '~/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard'; +import EmptyBiasChartSelectionCard from '~/pages/modelServing/screens/metrics/EmptyBiasChartSelectionCard'; +import DashboardExpandableSection from '~/concepts/dashboard/DashboardExpandableSection'; + +const SELECTED_CHARTS_STORAGE_KEY_PREFIX = 'odh.dashboard.xai.selected_bias_charts'; +const OPEN_WRAPPER_STORAGE_KEY_PREFIX = `odh.dashboard.xai.bias_metric_chart_wrapper_open`; +const BiasTab: React.FC = () => { + const { inferenceService } = useParams(); + + const { biasMetricConfigs, loaded } = useExplainabilityModelData(); + + const [selectedBiasConfigs, setSelectedBiasConfigs] = useBrowserStorage( + `${SELECTED_CHARTS_STORAGE_KEY_PREFIX}-${inferenceService}`, + [], + true, + true, + ); + + if (!loaded) { + return ( + + + + ); + } -const BiasTab = () => { - const { biasMetricConfigs } = useExplainabilityModelData(); return ( - + + + + Metrics to display + + + + + + + + + } + /> - {biasMetricConfigs.length === 0 ? ( + {(biasMetricConfigs.length === 0 && ( - ) : ( - <> - - - + )) || + (selectedBiasConfigs.length === 0 && ( - + - - )} + )) || ( + <> + {selectedBiasConfigs.map((x) => ( + + + + + + ))} + + )} ); }; - export default BiasTab; diff --git a/frontend/src/pages/modelServing/screens/metrics/DIRChart.tsx b/frontend/src/pages/modelServing/screens/metrics/DIRChart.tsx deleted file mode 100644 index cf1cee08f2..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/DIRChart.tsx +++ /dev/null @@ -1,42 +0,0 @@ -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 { MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; - -const DIRChart = () => { - const DEFAULT_MAX_THRESHOLD = 1.2; - const DEFAULT_MIN_THRESHOLD = 0.8; - const PADDING = 0.1; - - 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={(maxYValue) => ({ - y: - maxYValue > DEFAULT_MAX_THRESHOLD - ? [0, maxYValue + PADDING] - : [0, DEFAULT_MAX_THRESHOLD + PADDING], - })} - thresholds={[DEFAULT_MAX_THRESHOLD, DEFAULT_MIN_THRESHOLD]} - type={MetricsChartTypes.LINE} - /> - ); -}; - -export default DIRChart; diff --git a/frontend/src/pages/modelServing/screens/metrics/EmptyBiasChartSelectionCard.tsx b/frontend/src/pages/modelServing/screens/metrics/EmptyBiasChartSelectionCard.tsx new file mode 100644 index 0000000000..a8bc5cfa0c --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/EmptyBiasChartSelectionCard.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { + Card, + CardBody, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + Title, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import { + EMPTY_BIAS_CHART_SELECTION_DESC, + EMPTY_BIAS_CHART_SELECTION_TITLE, +} from '~/pages/modelServing/screens/metrics/const'; + +const EmptyBiasChartSelectionCard = () => ( + + + + + + {EMPTY_BIAS_CHART_SELECTION_TITLE} + + {EMPTY_BIAS_CHART_SELECTION_DESC} + + + +); + +export default EmptyBiasChartSelectionCard; diff --git a/frontend/src/pages/modelServing/screens/metrics/InferenceGraphs.tsx b/frontend/src/pages/modelServing/screens/metrics/InferenceGraphs.tsx index 920756b1c5..85757bea49 100644 --- a/frontend/src/pages/modelServing/screens/metrics/InferenceGraphs.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/InferenceGraphs.tsx @@ -27,12 +27,6 @@ const InferenceGraphs: React.FC = () => { { name: 'Failed http requests (x100)', metric: data[InferenceMetricType.REQUEST_COUNT_FAILED], - translatePoint: (point) => { - // TODO: remove when real values are used - const newPoint = per100(point); - const y = Math.floor(newPoint.y / (Math.floor(Math.random() * 2) + 2)); - return { ...newPoint, y }; - }, }, ]} title={`Http requests per ${inHours ? 'hour' : 'day'} (x100)`} diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx index 5df7897f26..b965a575b2 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx @@ -16,6 +16,7 @@ type MetricsPageProps = { const MetricsPage: React.FC = ({ title, breadcrumbItems }) => { const { tab } = useParams(); const navigate = useNavigate(); + return ( { > Performance} + title={Endpoint Performance} aria-label="Performance tab" className="odh-tabcontent-fix" > @@ -47,7 +47,7 @@ const MetricsPageTabs: React.FC = () => { Bias} + title={Model Bias} aria-label="Bias tab" className="odh-tabcontent-fix" actions={ diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx index 53e1b5010e..095710ee03 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx @@ -1,54 +1,87 @@ import * as React from 'react'; import { - Button, Select, SelectOption, - Text, + Stack, + StackItem, Toolbar, ToolbarContent, + ToolbarGroup, ToolbarItem, } from '@patternfly/react-core'; -import { SyncAltIcon } from '@patternfly/react-icons'; -import { TimeframeTitle } from '~/pages/modelServing/screens/types'; -import { relativeTime } from '~/utilities/time'; -import { isTimeframeTitle } from './utils'; +import { RefreshIntervalTitle, TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { isRefreshIntervalTitle, isTimeframeTitle } from './utils'; import { ModelServingMetricsContext } from './ModelServingMetricsContext'; -const MetricsPageToolbar: React.FC = () => { +type MetricsPageToolbarProps = { + leftToolbarItem?: React.ReactElement; +}; + +const MetricsPageToolbar: React.FC = ({ leftToolbarItem }) => { const [timeframeOpen, setTimeframeOpen] = React.useState(false); - const { currentTimeframe, setCurrentTimeframe, refresh, lastUpdateTime } = React.useContext( - ModelServingMetricsContext, - ); + const { + currentTimeframe, + setCurrentTimeframe, + currentRefreshInterval, + setCurrentRefreshInterval, + } = React.useContext(ModelServingMetricsContext); + + const [intervalOpen, setIntervalOpen] = React.useState(false); + return ( - + - - - - - - - - Last update -
- {relativeTime(Date.now(), lastUpdateTime)} -
+ {leftToolbarItem} + + + + + Time range + + + + + + + + + + Refresh interval + + + + + + +
); diff --git a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx b/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx index 48fc9ee694..c12d913147 100644 --- a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import { useModelServingMetrics } from '~/api'; import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; import { DEFAULT_CONTEXT_DATA } from '~/utilities/const'; -import { MetricType, TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { + MetricType, + RefreshIntervalTitle, + TimeframeTitle, +} from '~/pages/modelServing/screens/types'; export enum RuntimeMetricType { AVG_RESPONSE_TIME = 'runtime_avg-response-time', @@ -25,6 +29,8 @@ type ModelServingMetricsContext = { >; currentTimeframe: TimeframeTitle; setCurrentTimeframe: (timeframe: TimeframeTitle) => void; + currentRefreshInterval: RefreshIntervalTitle; + setCurrentRefreshInterval: (interval: RefreshIntervalTitle) => void; refresh: () => void; lastUpdateTime: number; setLastUpdateTime: (time: number) => void; @@ -43,6 +49,8 @@ export const ModelServingMetricsContext = React.createContext undefined, + currentRefreshInterval: RefreshIntervalTitle.FIVE_MINUTES, + setCurrentRefreshInterval: () => undefined, refresh: () => undefined, lastUpdateTime: 0, setLastUpdateTime: () => undefined, @@ -63,6 +71,10 @@ export const ModelServingMetricsProvider: React.FC( TimeframeTitle.ONE_DAY, ); + + const [currentRefreshInterval, setCurrentRefreshInterval] = React.useState( + RefreshIntervalTitle.FIVE_MINUTES, + ); const [lastUpdateTime, setLastUpdateTime] = React.useState(Date.now()); const { data, refresh } = useModelServingMetrics( @@ -71,6 +83,7 @@ export const ModelServingMetricsProvider: React.FC { - const DEFAULT_MAX_THRESHOLD = 0.1; - const DEFAULT_MIN_THRESHOLD = -0.1; - const PADDING = 0.1; - - return ( - - - Statistical Parity Difference (SPD) measures imbalances in classifications by - calculating the difference between the proportion of the majority and protected classes - getting a particular outcome. - - - Typically, -0.1 < SPD < 0.1 indicates a fair model, while a value outside those - bounds indicates an unfair model for the groups and outcomes in question. - - - } - domain={(maxYValue) => ({ - y: - maxYValue > DEFAULT_MAX_THRESHOLD - ? [-1 * maxYValue - PADDING, maxYValue + PADDING] - : [DEFAULT_MIN_THRESHOLD - PADDING, DEFAULT_MAX_THRESHOLD + PADDING], - })} - thresholds={[DEFAULT_MAX_THRESHOLD, DEFAULT_MIN_THRESHOLD]} - /> - ); -}; -export default SPDChart; diff --git a/frontend/src/pages/modelServing/screens/metrics/ScheduledMetricSelect.tsx b/frontend/src/pages/modelServing/screens/metrics/ScheduledMetricSelect.tsx deleted file mode 100644 index f27b4e571d..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/ScheduledMetricSelect.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Select, SelectOption } from '@patternfly/react-core'; - -type ScheduledMetricSelectProps = { - selected?: string; - options: string[]; - onSelect: (name: string) => void; -}; -const ScheduledMetricSelect: React.FC = ({ - selected, - options, - onSelect, -}) => { - const [isOpen, setOpen] = React.useState(false); - - return ( - - ); -}; -export default ScheduledMetricSelect; diff --git a/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx b/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx index 3890555cd3..795a76f2d6 100644 --- a/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx @@ -1,60 +1,32 @@ 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'; +import { ModelServingMetricsContext } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import { BiasMetricConfig } from '~/concepts/explainability/types'; +import { createChartThresholds } from '~/pages/modelServing/screens/metrics/utils'; +import { BIAS_CHART_CONFIGS } from '~/pages/modelServing/screens/metrics/const'; -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; +export type TrustyChartProps = { + biasMetricConfig: BiasMetricConfig; }; -const TrustyChart: React.FC = ({ - title, - abbreviation, - metricType, - tooltip, - thresholds, - domain, - type = MetricsChartTypes.AREA, -}) => { - const THRESHOLD_COLOR = 'red'; +const TrustyChart: React.FC = ({ biasMetricConfig }) => { const { data } = React.useContext(ModelServingMetricsContext); - const [selectedPayloadName, setSelectedPayloadName] = React.useState(); - const metricData = data[metricType].data; + const { id, metricType, thresholdDelta } = biasMetricConfig; - //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?.requestName) { - x.metric.requestName = `Payload ${i}`; - } - }); + const { title, abbreviation, inferenceMetricKey, chartType, domainCalculator } = + BIAS_CHART_CONFIGS[metricType]; - React.useEffect(() => { - if (!selectedPayloadName) { - setSelectedPayloadName(metricData[0]?.metric?.requestName); - } - }, [selectedPayloadName, metricData]); + const metric = React.useMemo(() => { + const metricData = data[inferenceMetricKey].data; - const payloadOptions: string[] = metricData.map((payload) => payload.metric.requestName); + const values = metricData.find((x) => x.metric.request === id)?.values; - const selectedPayload = metricData.find((x) => x.metric.requestName === selectedPayloadName); - - const metric = { - ...data[metricType], - data: selectedPayload?.values, - }; + return { + ...data[inferenceMetricKey], + data: values, + }; + }, [data, id, inferenceMetricKey]); return ( = ({ name: abbreviation, metric: metric, }} - domain={domain} - toolbar={ - - {tooltip && ( - - - - - - )} - {/* - - ) : undefined + isGlobal ? : undefined } rowRenderer={(is) => ( ( - - +type BiasConfigurationEmptyStateProps = { + actionButton: React.ReactNode; + variant: EmptyStateVariant; +}; + +const BiasConfigurationEmptyState: React.FC = ({ + actionButton, + variant, +}) => ( + + - No bias metrics configured + {EMPTY_BIAS_CONFIGURATION_TITLE} - - No bias metrics for this model have been configured. To monitor model bias, you must first - configure metrics - + {EMPTY_BIAS_CONFIGURATION_DESC} + {actionButton} ); diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx index 79b46a87e4..d4fb10bf6b 100644 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx @@ -1,5 +1,11 @@ import * as React from 'react'; -import { Breadcrumb, Button } from '@patternfly/react-core'; +import { + Breadcrumb, + Button, + EmptyStateVariant, + PageSection, + PageSectionVariants, +} from '@patternfly/react-core'; import { useNavigate } from 'react-router-dom'; import ApplicationsPage from '~/pages/ApplicationsPage'; import { BreadcrumbItemType } from '~/types'; @@ -9,6 +15,8 @@ import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/glo import { MetricsTabKeys } from './types'; import BiasConfigurationTable from './BiasConfigurationTable'; import { getBreadcrumbItemComponents } from './utils'; +import BiasConfigurationEmptyState from './BiasConfigurationEmptyState'; +import ManageBiasConfigurationModal from './biasConfigurationModal/ManageBiasConfigurationModal'; type BiasConfigurationPageProps = { breadcrumbItems: BreadcrumbItemType[]; @@ -19,27 +27,63 @@ const BiasConfigurationPage: React.FC = ({ breadcrumbItems, inferenceService, }) => { - const { biasMetricConfigs, loaded } = useExplainabilityModelData(); + const { biasMetricConfigs, loaded, loadError, refresh } = useExplainabilityModelData(); const navigate = useNavigate(); - return ( - {getBreadcrumbItemComponents(breadcrumbItems)}} - headerAction={ - + const firstRender = React.useRef(true); + const [isOpen, setOpen] = React.useState(false); + + React.useEffect(() => { + if (loaded && !loadError) { + if (firstRender.current) { + firstRender.current = false; + if (biasMetricConfigs.length === 0) { + setOpen(true); + } } - loaded={loaded} - provideChildrenPadding - // The page is not empty, we will handle the empty state in the table - empty={false} - > - - + } + }, [loaded, biasMetricConfigs, loadError]); + + return ( + <> + {getBreadcrumbItemComponents(breadcrumbItems)}} + headerAction={ + + } + loaded={loaded} + provideChildrenPadding + empty={biasMetricConfigs.length === 0} + emptyStatePage={ + + setOpen(true)}>Configure metric} + variant={EmptyStateVariant.large} + /> + + } + > + setOpen(true)} + /> + + { + if (submit) { + refresh(); + } + setOpen(false); + }} + inferenceService={inferenceService} + /> + ); }; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx index 78a848eefb..98e53b4ea9 100644 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx @@ -1,22 +1,25 @@ import * as React from 'react'; -import { Button, ToolbarItem } from '@patternfly/react-core'; +import { Button, ButtonVariant, ToolbarItem } from '@patternfly/react-core'; import Table from '~/components/table/Table'; import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField'; import { BiasMetricConfig } from '~/concepts/explainability/types'; import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; import { InferenceServiceKind } from '~/k8sTypes'; import DeleteBiasConfigurationModal from '~/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal'; +import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; import ManageBiasConfigurationModal from './biasConfigurationModal/ManageBiasConfigurationModal'; import BiasConfigurationTableRow from './BiasConfigurationTableRow'; import { columns } from './tableData'; -import BiasConfigurationEmptyState from './BiasConfigurationEmptyState'; -import BiasConfigurationButton from './BiasConfigurationButton'; type BiasConfigurationTableProps = { inferenceService: InferenceServiceKind; + onConfigure: () => void; }; -const BiasConfigurationTable: React.FC = ({ inferenceService }) => { +const BiasConfigurationTable: React.FC = ({ + inferenceService, + onConfigure, +}) => { const { biasMetricConfigs, refresh } = useExplainabilityModelData(); const [searchType, setSearchType] = React.useState(SearchType.NAME); const [search, setSearch] = React.useState(''); @@ -75,18 +78,7 @@ const BiasConfigurationTable: React.FC = ({ inferen onDeleteConfiguration={setDeleteConfiguration} /> )} - emptyTableView={ - search ? ( - <> - No metric configurations match your filters.{' '} - - - ) : ( - - ) - } + emptyTableView={} toolbarContent={ <> @@ -103,7 +95,9 @@ const BiasConfigurationTable: React.FC = ({ inferen /> - + } diff --git a/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx b/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx index 05d6d5d9ef..e43feefffc 100644 --- a/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx @@ -1,36 +1,25 @@ import * as React from 'react'; -import { - Button, - Card, - CardBody, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - Title, -} from '@patternfly/react-core'; -import { WrenchIcon } from '@patternfly/react-icons'; +import { Button, Card, CardBody, EmptyStateVariant } from '@patternfly/react-core'; import { useNavigate } from 'react-router-dom'; -import { EMPTY_BIAS_CONFIGURATION_DESC, EMPTY_BIAS_CONFIGURATION_TITLE } from './const'; +import BiasConfigurationEmptyState from './BiasConfigurationEmptyState'; const EmptyBiasConfigurationCard: React.FC = () => { const navigate = useNavigate(); return ( - - - - {EMPTY_BIAS_CONFIGURATION_TITLE} - - {EMPTY_BIAS_CONFIGURATION_DESC} - - + { + navigate('../configure', { relative: 'path' }); + }} + > + Configure + + } + variant={EmptyStateVariant.full} + /> ); diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx index 392516754f..bcccd9be86 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx @@ -3,26 +3,33 @@ import { useParams, useNavigate } from 'react-router-dom'; import { Tabs, Tab, TabTitleText, TabAction } from '@patternfly/react-core'; import { MetricsTabKeys } from '~/pages/modelServing/screens/metrics/types'; import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; +import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; +import NotFound from '~/pages/NotFound'; import PerformanceTab from './PerformanceTab'; import BiasTab from './BiasTab'; import BiasConfigurationAlertPopover from './BiasConfigurationAlertPopover'; +import useMetricsPageEnabledTabs from './useMetricsPageEnabledTabs'; import './MetricsPageTabs.scss'; const MetricsPageTabs: React.FC = () => { - const DEFAULT_TAB = MetricsTabKeys.PERFORMANCE; + const enabledTabs = useMetricsPageEnabledTabs(); const { biasMetricConfigs, loaded } = useExplainabilityModelData(); - + const [biasMetricsEnabled] = useBiasMetricsEnabled(); const { tab } = useParams<{ tab: MetricsTabKeys }>(); const navigate = useNavigate(); React.useEffect(() => { if (!tab) { - navigate(`./${DEFAULT_TAB}`, { replace: true }); - } else if (!Object.values(MetricsTabKeys).includes(tab)) { - navigate(`../${DEFAULT_TAB}`, { replace: true }); + navigate(`./${enabledTabs[0]}`, { replace: true }); + } else if (!enabledTabs.includes(tab)) { + navigate(`../${enabledTabs[0]}`, { replace: true }); } - }, [DEFAULT_TAB, navigate, tab]); + }, [enabledTabs, navigate, tab]); + + if (enabledTabs.length === 0) { + return ; + } return ( { >
- Model Bias} - aria-label="Bias tab" - className="odh-tabcontent-fix" - actions={ - loaded && - biasMetricConfigs.length === 0 && ( - - { - navigate('../configure', { relative: 'path' }); - }} - /> - - ) - } - > - - + {biasMetricsEnabled && ( + Model Bias} + aria-label="Bias tab" + className="odh-tabcontent-fix" + actions={ + loaded && + biasMetricConfigs.length === 0 && ( + + { + navigate('../configure', { relative: 'path' }); + }} + /> + + ) + } + > + + + )} ); }; diff --git a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx index 21d45e8934..29a79cd426 100644 --- a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -import { Form, FormGroup, Modal, TextInput, Tooltip } from '@patternfly/react-core'; -import { HelpIcon } from '@patternfly/react-icons'; +import { Form, FormGroup, Modal, TextInput } from '@patternfly/react-core'; import { BiasMetricConfig } from '~/concepts/explainability/types'; import { MetricTypes } from '~/api'; import { InferenceServiceKind } from '~/k8sTypes'; @@ -9,7 +8,9 @@ import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; import { checkConfigurationFieldsValid, convertConfigurationRequestType, + getThresholdDefaultDelta, } from '~/pages/modelServing/screens/metrics/utils'; +import DashboardHelpTooltip from '~/concepts/dashboard/DashboardHelpTooltip'; import useBiasConfigurationObject from './useBiasConfigurationObject'; import MetricTypeField from './MetricTypeField'; @@ -29,13 +30,14 @@ const ManageBiasConfigurationModal: React.FC const { apiState: { api }, } = React.useContext(ExplainabilityContext); - const [configuration, setConfiguration] = useBiasConfigurationObject( - inferenceService.metadata.name, - existingConfiguration, - ); const [actionInProgress, setActionInProgress] = React.useState(false); const [error, setError] = React.useState(); const [metricType, setMetricType] = React.useState(); + const [configuration, setConfiguration, resetData] = useBiasConfigurationObject( + inferenceService.metadata.name, + metricType, + existingConfiguration, + ); React.useEffect(() => { setMetricType(existingConfiguration?.metricType); @@ -45,6 +47,8 @@ const ManageBiasConfigurationModal: React.FC onClose(submitted); setError(undefined); setActionInProgress(false); + resetData(); + setMetricType(undefined); }; const onCreateConfiguration = () => { @@ -90,36 +94,73 @@ const ManageBiasConfigurationModal: React.FC onChange={(value) => setConfiguration('requestName', value)} /> - - + { + setMetricType(value); + setConfiguration('thresholdDelta', getThresholdDefaultDelta(value)); + }} + /> + + } + > setConfiguration('protectedAttribute', value)} /> - + + } + > setConfiguration('privilegedAttribute', value)} /> - + + } + > setConfiguration('unprivilegedAttribute', value)} /> - + + } + > setConfiguration('outcomeName', value)} /> - + + } + > label="Violation threshold" fieldId="violation-threshold" labelIcon={ - - - + } > label="Metric batch size" fieldId="metric-batch-size" labelIcon={ - - - + } > void; + onChange: (value: MetricTypes) => void; }; -const MetricTypeField: React.FC = ({ fieldId, value, setValue }) => { +const MetricTypeField: React.FC = ({ fieldId, value, onChange }) => { const [isOpen, setOpen] = React.useState(false); return ( - // TODO: decide what to show in the helper tooltip - - - - } - > +
@@ -81,7 +81,7 @@ const PipelineRunTable: React.FC = ({ runs, experiments } columns={pipelineRunExperimentColumns} experiments={experiments} enablePagination - emptyTableView={} + emptyTableView={} toolbarContent={toolbarContent} rowRenderer={(resource) => ( = ({ jobs, experiment data={filterJobs} columns={pipelineRunJobColumns} enablePagination - emptyTableView={} + emptyTableView={} toolbarContent={toolbarContent} rowRenderer={(job) => ( @@ -82,7 +82,7 @@ const PipelineRunJobTable: React.FC = ({ jobs, experiment experiments={experiments} columns={pipelineRunJobExperimentColumns} enablePagination - emptyTableView={} + emptyTableView={} toolbarContent={toolbarContent} rowRenderer={(resource) => ( Promise; refreshAPIState: () => void; - apiState: PipelineAPIState; + apiState: APIState; }; const PipelinesContext = React.createContext({ @@ -41,7 +41,7 @@ const PipelinesContext = React.createContext({ project: null as unknown as ProjectKind, refreshState: async () => undefined, refreshAPIState: () => undefined, - apiState: { apiAvailable: false, api: null as unknown as PipelineAPIState['api'] }, + apiState: { apiAvailable: false, api: null as unknown as APIState['api'] }, }); type PipelineContextProviderProps = { @@ -78,7 +78,7 @@ export const PipelineContextProvider: React.FC = ( [refreshRoute, refreshCR], ); - const [apiState, refreshAPIState] = usePipelineAPIState(hostPath); + const [apiState, refreshAPIState] = useAPIState(hostPath); let error = crLoadError || routeLoadError; if (error || !project) { @@ -111,7 +111,7 @@ export const PipelineContextProvider: React.FC = ( ); }; -type UsePipelinesAPI = PipelineAPIState & { +type UsePipelinesAPI = APIState & { /** The contextual namespace */ namespace: string; /** The Project resource behind the namespace */ diff --git a/frontend/src/concepts/pipelines/context/useAPIState.ts b/frontend/src/concepts/pipelines/context/useAPIState.ts new file mode 100644 index 0000000000..1e809c874b --- /dev/null +++ b/frontend/src/concepts/pipelines/context/useAPIState.ts @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { + createExperiment, + createPipelineRun, + createPipelineRunJob, + deleteExperiment, + deletePipeline, + deletePipelineRun, + deletePipelineRunJob, + getExperiment, + getPipeline, + getPipelineRun, + getPipelineRunJob, + listExperiments, + listPipelineRunJobs, + listPipelineRuns, + listPipelineRunsByPipeline, + listPipelines, + listPipelineTemplates, + stopPipelineRun, + updatePipelineRunJob, + uploadPipeline, +} from '~/api'; +import { PipelineAPIs } from '~/concepts/pipelines/types'; + +export type APIState = { + /** If API will successfully call */ + apiAvailable: boolean; + /** The available API functions */ + api: PipelineAPIs; +}; + +const useAPIState = ( + hostPath: string | null, +): [apiState: APIState, refreshAPIState: () => void] => { + const [internalAPIToggleState, setInternalAPIToggleState] = React.useState(false); + + const refreshAPIState = React.useCallback(() => { + setInternalAPIToggleState((v) => !v); + }, []); + + const apiState = React.useMemo(() => { + // Note: This is a hack usage to get around the linter -- avoid copying this logic + // eslint-disable-next-line no-console + console.log('Computing Pipeline API', internalAPIToggleState ? '' : ''); + + let path = hostPath; + if (!path) { + // TODO: we need to figure out maybe a stopgap or something + path = ''; + } + + return { + apiAvailable: !!path, + api: { + createExperiment: createExperiment(path), + createPipelineRun: createPipelineRun(path), + createPipelineRunJob: createPipelineRunJob(path), + getExperiment: getExperiment(path), + deleteExperiment: deleteExperiment(path), + getPipeline: getPipeline(path), + getPipelineRun: getPipelineRun(path), + getPipelineRunJob: getPipelineRunJob(path), + deletePipeline: deletePipeline(path), + deletePipelineRun: deletePipelineRun(path), + deletePipelineRunJob: deletePipelineRunJob(path), + listExperiments: listExperiments(path), + listPipelines: listPipelines(path), + listPipelineRuns: listPipelineRuns(path), + listPipelineRunJobs: listPipelineRunJobs(path), + listPipelineRunsByPipeline: listPipelineRunsByPipeline(path), + listPipelineTemplate: listPipelineTemplates(path), + stopPipelineRun: stopPipelineRun(path), + updatePipelineRunJob: updatePipelineRunJob(path), + uploadPipeline: uploadPipeline(path), + }, + }; + }, [hostPath, internalAPIToggleState]); + + return [apiState, refreshAPIState]; +}; + +export default useAPIState; diff --git a/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts b/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts deleted file mode 100644 index 323d403259..0000000000 --- a/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { - createExperiment, - createPipelineRun, - createPipelineRunJob, - deleteExperiment, - deletePipeline, - deletePipelineRun, - deletePipelineRunJob, - getExperiment, - getPipeline, - getPipelineRun, - getPipelineRunJob, - listExperiments, - listPipelineRunJobs, - listPipelineRuns, - listPipelineRunsByPipeline, - listPipelines, - listPipelineTemplates, - stopPipelineRun, - updatePipelineRunJob, - uploadPipeline, -} from '~/api'; -import { PipelineAPIs } from '~/concepts/pipelines/types'; -import { APIState } from '~/concepts/proxy/types'; -import useAPIState from '~/concepts/proxy/useAPIState'; - -export type PipelineAPIState = APIState; - -const usePipelineAPIState = ( - hostPath: string | null, -): [apiState: PipelineAPIState, refreshAPIState: () => void] => { - const createAPI = React.useCallback( - (path) => ({ - createExperiment: createExperiment(path), - createPipelineRun: createPipelineRun(path), - createPipelineRunJob: createPipelineRunJob(path), - getExperiment: getExperiment(path), - deleteExperiment: deleteExperiment(path), - getPipeline: getPipeline(path), - getPipelineRun: getPipelineRun(path), - getPipelineRunJob: getPipelineRunJob(path), - deletePipeline: deletePipeline(path), - deletePipelineRun: deletePipelineRun(path), - deletePipelineRunJob: deletePipelineRunJob(path), - listExperiments: listExperiments(path), - listPipelines: listPipelines(path), - listPipelineRuns: listPipelineRuns(path), - listPipelineRunJobs: listPipelineRunJobs(path), - listPipelineRunsByPipeline: listPipelineRunsByPipeline(path), - listPipelineTemplate: listPipelineTemplates(path), - stopPipelineRun: stopPipelineRun(path), - updatePipelineRunJob: updatePipelineRunJob(path), - uploadPipeline: uploadPipeline(path), - }), - [], - ); - - return useAPIState(hostPath, createAPI); -}; - -export default usePipelineAPIState; diff --git a/frontend/src/concepts/proxy/types.ts b/frontend/src/concepts/proxy/types.ts deleted file mode 100644 index 7e23db7d72..0000000000 --- a/frontend/src/concepts/proxy/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type APIState = { - /** If API will successfully call */ - apiAvailable: boolean; - /** The available API functions */ - api: T; -}; diff --git a/frontend/src/concepts/proxy/useAPIState.ts b/frontend/src/concepts/proxy/useAPIState.ts deleted file mode 100644 index 4fbdece26f..0000000000 --- a/frontend/src/concepts/proxy/useAPIState.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import { APIState } from '~/concepts/proxy/types'; - -const useAPIState = ( - hostPath: string | null, - createAPI: (path: string) => T, -): [apiState: APIState, refreshAPIState: () => void] => { - const [internalAPIToggleState, setInternalAPIToggleState] = React.useState(false); - - const refreshAPIState = React.useCallback(() => { - setInternalAPIToggleState((v) => !v); - }, []); - - const apiState = React.useMemo>(() => { - // Note: This is a hack usage to get around the linter -- avoid copying this logic - // eslint-disable-next-line no-console - console.log('Computing API', internalAPIToggleState ? '' : ''); - - let path = hostPath; - if (!path) { - // TODO: we need to figure out maybe a stopgap or something - path = ''; - } - const api = createAPI(path); - - return { - apiAvailable: !!path, - api, - }; - }, [createAPI, hostPath, internalAPIToggleState]); - - return [apiState, refreshAPIState]; -}; - -export default useAPIState; diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index cb7d731b4d..dc2b3f209b 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -202,7 +202,6 @@ export type ImageStreamSpecTagType = { export type K8sAPIOptions = { dryRun?: boolean; signal?: AbortSignal; - parseJSON?: boolean; }; /** A status object when Kube backend can't handle a request. */ @@ -405,9 +404,6 @@ export type RouteKind = K8sResourceCommon & { spec: { host: string; path: string; - port: { - targetPort: string; - }; }; }; @@ -431,34 +427,6 @@ export type AWSSecretKind = SecretKind & { data: Record; }; -export type TrustyAIKind = K8sResourceCommon & { - metadata: { - name: string; - namespace: string; - }; - spec: { - storage: { - format: string; - folder: string; - size: string; - }; - data: { - filename: string; - format: string; - }; - metrics: { - schedule: string; - batchSize?: number; - }; - }; - status?: { - conditions?: K8sCondition[]; - phase?: string; - ready?: string; - replicas?: number; - }; -}; - export type DSPipelineKind = K8sResourceCommon & { metadata: { name: string; diff --git a/frontend/src/pages/ApplicationsPage.tsx b/frontend/src/pages/ApplicationsPage.tsx index 51e4622df8..132e97fd71 100644 --- a/frontend/src/pages/ApplicationsPage.tsx +++ b/frontend/src/pages/ApplicationsPage.tsx @@ -21,7 +21,8 @@ import { type ApplicationsPageProps = { title: React.ReactNode; breadcrumb?: React.ReactNode; - description: React.ReactNode; + toolbar?: React.ReactNode; + description?: React.ReactNode; loaded: boolean; empty: boolean; loadError?: Error; @@ -37,6 +38,7 @@ type ApplicationsPageProps = { const ApplicationsPage: React.FC = ({ title, breadcrumb, + toolbar, description, loaded, empty, @@ -65,6 +67,8 @@ const ApplicationsPage: React.FC = ({ {headerContent && {headerContent}} + {/* Deprecated */} + {toolbar} ); diff --git a/frontend/src/pages/modelServing/ModelServingRoutes.tsx b/frontend/src/pages/modelServing/ModelServingRoutes.tsx index 74cd912ed1..94bb4d211b 100644 --- a/frontend/src/pages/modelServing/ModelServingRoutes.tsx +++ b/frontend/src/pages/modelServing/ModelServingRoutes.tsx @@ -1,37 +1,24 @@ import * as React from 'react'; import { Navigate, Route } from 'react-router-dom'; import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; -import ModelServingExplainabilityWrapper from '~/pages/modelServing/screens/metrics/ModelServingExplainabilityWrapper'; -import BiasConfigurationBreadcrumbPage from './screens/metrics/BiasConfigurationBreadcrumbPage'; -import GlobalModelMetricsPage from './screens/metrics/GlobalModelMetricsPage'; import ModelServingContextProvider from './ModelServingContext'; -import GlobalModelMetricsWrapper from './screens/metrics/GlobalModelMetricsWrapper'; +import ModelServingMetricsWrapper from './screens/metrics/ModelServingMetricsWrapper'; import ModelServingGlobal from './screens/global/ModelServingGlobal'; import useModelMetricsEnabled from './useModelMetricsEnabled'; const ModelServingRoutes: React.FC = () => { const [modelMetricsEnabled] = useModelMetricsEnabled(); - const [biasMetricsEnabled] = useBiasMetricsEnabled(); - //TODO: Split route to project and mount provider here. This will allow you to load data when model switching is later implemented. return ( }> } /> - {modelMetricsEnabled && ( - }> - } /> - }> - } /> - {biasMetricsEnabled && ( - } /> - )} - - {/* TODO: Global Runtime metrics?? */} - } /> - - )} + : + } + /> } /> diff --git a/frontend/src/pages/modelServing/screens/const.ts b/frontend/src/pages/modelServing/screens/const.ts index c1f0ccf91a..e862400856 100644 --- a/frontend/src/pages/modelServing/screens/const.ts +++ b/frontend/src/pages/modelServing/screens/const.ts @@ -1,11 +1,4 @@ -import { - RefreshIntervalTitle, - RefreshIntervalValueType, - ServingRuntimeSize, - TimeframeStepType, - TimeframeTimeType, - TimeframeTitle, -} from './types'; +import { ServingRuntimeSize, TimeframeStepType, TimeframeTimeType, TimeframeTitle } from './types'; export const DEFAULT_MODEL_SERVER_SIZES: ServingRuntimeSize[] = [ { @@ -64,42 +57,18 @@ export const STORAGE_KEYS_REQUIRED: STORAGE_KEYS[] = [ STORAGE_KEYS.S3_ENDPOINT, ]; -/** - * The desired range (x-axis) of the charts. - * Unit is in seconds - */ -export const TimeframeTimeRange: TimeframeTimeType = { +// unit: seconds +export const TimeframeTime: TimeframeTimeType = { + [TimeframeTitle.FIVE_MINUTES]: 5 * 60, [TimeframeTitle.ONE_HOUR]: 60 * 60, [TimeframeTitle.ONE_DAY]: 24 * 60 * 60, [TimeframeTitle.ONE_WEEK]: 7 * 24 * 60 * 60, - [TimeframeTitle.ONE_MONTH]: 30 * 7 * 24 * 60 * 60, - // [TimeframeTitle.UNLIMITED]: 0, }; -/** - * How large a step is -- value is in how many seconds to combine to great an individual data response - * Each should be getting ~300 data points (assuming data fills the gap) - * - * eg. [TimeframeTitle.ONE_DAY]: 24 * 12, - * 24h * 60m * 60s => 86,400 seconds of space - * 86,400 / (24 * 12) => 300 points of prometheus data - */ +// make sure we always get ~300 data points export const TimeframeStep: TimeframeStepType = { + [TimeframeTitle.FIVE_MINUTES]: 1, [TimeframeTitle.ONE_HOUR]: 12, [TimeframeTitle.ONE_DAY]: 24 * 12, [TimeframeTitle.ONE_WEEK]: 7 * 24 * 12, - [TimeframeTitle.ONE_MONTH]: 30 * 24 * 12, - // [TimeframeTitle.UNLIMITED]: 30 * 7 * 24 * 12, // TODO: determine if we "zoom out" more -}; - -export const RefreshIntervalValue: RefreshIntervalValueType = { - [RefreshIntervalTitle.FIFTEEN_SECONDS]: 15 * 1000, - [RefreshIntervalTitle.THIRTY_SECONDS]: 30 * 1000, - [RefreshIntervalTitle.ONE_MINUTE]: 60 * 1000, - [RefreshIntervalTitle.FIVE_MINUTES]: 5 * 60 * 1000, - [RefreshIntervalTitle.FIFTEEN_MINUTES]: 15 * 60 * 1000, - [RefreshIntervalTitle.THIRTY_MINUTES]: 30 * 60 * 1000, - [RefreshIntervalTitle.ONE_HOUR]: 60 * 60 * 1000, - [RefreshIntervalTitle.TWO_HOURS]: 2 * 60 * 60 * 1000, - [RefreshIntervalTitle.ONE_DAY]: 24 * 60 * 60 * 1000, }; diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceListView.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceListView.tsx index ed2853f1e0..416bdeed1c 100644 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceListView.tsx +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceListView.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { ToolbarItem } from '@patternfly/react-core'; import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; +import SearchField, { SearchType } from '~/pages/projects/components/SearchField'; import { ModelServingContext } from '~/pages/modelServing/ModelServingContext'; import { ProjectsContext } from '~/concepts/projects/ProjectsContext'; -import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField'; import { getInferenceServiceDisplayName, getInferenceServiceProjectDisplayName } from './utils'; import ServeModelButton from './ServeModelButton'; import InferenceServiceTable from './InferenceServiceTable'; @@ -64,7 +64,7 @@ const InferenceServiceListView: React.FC = ({ toolbarContent={ <> - = ({ toolbarContent={toolbarContent} enablePagination={enablePagination} emptyTableView={ - isGlobal ? : undefined + isGlobal ? ( + <> + No projects match your filters.{' '} + + + ) : undefined } rowRenderer={(is) => ( void; -}; - -const BiasConfigurationAlertPopover: React.FC = ({ - onConfigure, -}) => ( - } - bodyContent={EMPTY_BIAS_CONFIGURATION_DESC} - footerContent={ - - } - > - - - - -); - -export default BiasConfigurationAlertPopover; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationBreadcrumbPage.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationBreadcrumbPage.tsx deleted file mode 100644 index 78fc94c96b..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationBreadcrumbPage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; -import { useOutletContext } from 'react-router-dom'; -import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; -import BiasConfigurationPage from './BiasConfigurationPage'; -import { GlobalModelMetricsOutletContextProps } from './GlobalModelMetricsWrapper'; - -const BiasConfigurationBreadcrumbPage: React.FC = () => { - const { model, projectName } = useOutletContext(); - const modelDisplayName = getInferenceServiceDisplayName(model); - return ( - - ); -}; - -export default BiasConfigurationBreadcrumbPage; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationButton.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationButton.tsx deleted file mode 100644 index 572b6ed45a..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationButton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { Button } from '@patternfly/react-core'; -import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; -import { InferenceServiceKind } from '~/k8sTypes'; -import ManageBiasConfigurationModal from './biasConfigurationModal/ManageBiasConfigurationModal'; - -type BiasConfigurationButtonProps = { - inferenceService: InferenceServiceKind; -}; - -const BiasConfigurationButton: React.FC = ({ inferenceService }) => { - const [isOpen, setOpen] = React.useState(false); - const { biasMetricConfigs, loaded, refresh } = useExplainabilityModelData(); - - React.useEffect(() => { - if (loaded && biasMetricConfigs.length === 0) { - setOpen(true); - } - }, [loaded, biasMetricConfigs]); - - return ( - <> - - { - if (submit) { - refresh(); - } - setOpen(false); - }} - inferenceService={inferenceService} - /> - - ); -}; - -export default BiasConfigurationButton; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationEmptyState.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationEmptyState.tsx deleted file mode 100644 index 111960d1fe..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationEmptyState.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { - EmptyState, - EmptyStateBody, - EmptyStateIcon, - EmptyStateVariant, - Title, -} from '@patternfly/react-core'; -import { WrenchIcon } from '@patternfly/react-icons'; -import { EMPTY_BIAS_CONFIGURATION_DESC, EMPTY_BIAS_CONFIGURATION_TITLE } from './const'; - -type BiasConfigurationEmptyStateProps = { - actionButton: React.ReactNode; - variant: EmptyStateVariant; -}; - -const BiasConfigurationEmptyState: React.FC = ({ - actionButton, - variant, -}) => ( - - - - {EMPTY_BIAS_CONFIGURATION_TITLE} - - {EMPTY_BIAS_CONFIGURATION_DESC} - {actionButton} - -); - -export default BiasConfigurationEmptyState; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx deleted file mode 100644 index d4fb10bf6b..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationPage.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import * as React from 'react'; -import { - Breadcrumb, - Button, - EmptyStateVariant, - PageSection, - PageSectionVariants, -} from '@patternfly/react-core'; -import { useNavigate } from 'react-router-dom'; -import ApplicationsPage from '~/pages/ApplicationsPage'; -import { BreadcrumbItemType } from '~/types'; -import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; -import { InferenceServiceKind } from '~/k8sTypes'; -import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; -import { MetricsTabKeys } from './types'; -import BiasConfigurationTable from './BiasConfigurationTable'; -import { getBreadcrumbItemComponents } from './utils'; -import BiasConfigurationEmptyState from './BiasConfigurationEmptyState'; -import ManageBiasConfigurationModal from './biasConfigurationModal/ManageBiasConfigurationModal'; - -type BiasConfigurationPageProps = { - breadcrumbItems: BreadcrumbItemType[]; - inferenceService: InferenceServiceKind; -}; - -const BiasConfigurationPage: React.FC = ({ - breadcrumbItems, - inferenceService, -}) => { - const { biasMetricConfigs, loaded, loadError, refresh } = useExplainabilityModelData(); - const navigate = useNavigate(); - const firstRender = React.useRef(true); - const [isOpen, setOpen] = React.useState(false); - - React.useEffect(() => { - if (loaded && !loadError) { - if (firstRender.current) { - firstRender.current = false; - if (biasMetricConfigs.length === 0) { - setOpen(true); - } - } - } - }, [loaded, biasMetricConfigs, loadError]); - - return ( - <> - {getBreadcrumbItemComponents(breadcrumbItems)}} - headerAction={ - - } - loaded={loaded} - provideChildrenPadding - empty={biasMetricConfigs.length === 0} - emptyStatePage={ - - setOpen(true)}>Configure metric} - variant={EmptyStateVariant.large} - /> - - } - > - setOpen(true)} - /> - - { - if (submit) { - refresh(); - } - setOpen(false); - }} - inferenceService={inferenceService} - /> - - ); -}; - -export default BiasConfigurationPage; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx deleted file mode 100644 index 98e53b4ea9..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTable.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import * as React from 'react'; -import { Button, ButtonVariant, ToolbarItem } from '@patternfly/react-core'; -import Table from '~/components/table/Table'; -import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; -import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; -import { InferenceServiceKind } from '~/k8sTypes'; -import DeleteBiasConfigurationModal from '~/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal'; -import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; -import ManageBiasConfigurationModal from './biasConfigurationModal/ManageBiasConfigurationModal'; -import BiasConfigurationTableRow from './BiasConfigurationTableRow'; -import { columns } from './tableData'; - -type BiasConfigurationTableProps = { - inferenceService: InferenceServiceKind; - onConfigure: () => void; -}; - -const BiasConfigurationTable: React.FC = ({ - inferenceService, - onConfigure, -}) => { - const { biasMetricConfigs, refresh } = useExplainabilityModelData(); - const [searchType, setSearchType] = React.useState(SearchType.NAME); - const [search, setSearch] = React.useState(''); - const [cloneConfiguration, setCloneConfiguration] = React.useState(); - const [deleteConfiguration, setDeleteConfiguration] = React.useState(); - - const filteredConfigurations = biasMetricConfigs.filter((configuration) => { - if (!search) { - return true; - } - - switch (searchType) { - case SearchType.NAME: - return configuration.name.toLowerCase().includes(search.toLowerCase()); - case SearchType.METRIC: - return configuration.metricType.toLowerCase().includes(search.toLocaleLowerCase()); - case SearchType.PROTECTED_ATTRIBUTE: - return configuration.protectedAttribute.toLowerCase().includes(search.toLowerCase()); - case SearchType.OUTPUT: - return configuration.outcomeName.toLowerCase().includes(search.toLowerCase()); - default: - return true; - } - }); - - const resetFilters = () => { - setSearch(''); - }; - - // TODO: decide what we want to search - // Or should we reuse the complex filter search - const searchTypes = React.useMemo( - () => - Object.keys(SearchType).filter( - (key) => - SearchType[key] === SearchType.NAME || - SearchType[key] === SearchType.METRIC || - SearchType[key] === SearchType.PROTECTED_ATTRIBUTE || - SearchType[key] === SearchType.OUTPUT, - ), - [], - ); - return ( - <> -
{obj.outcomeName} {obj.favorableOutcome} - {/* TODO: add actions */} - + { + onCloneConfiguration(obj); + }, + }, + { + title: 'Delete', + onClick: () => { + onDeleteConfiguration(obj); + }, + }, + ]} + />
( - - )} - emptyTableView={} - toolbarContent={ - <> - - { - setSearchType(searchType); - }} - onSearchValueChange={(searchValue) => { - setSearch(searchValue); - }} - /> - - - - - - } - /> - { - if (submit) { - refresh(); - } - setCloneConfiguration(undefined); - }} - inferenceService={inferenceService} - /> - { - if (deleted) { - refresh(); - } - setDeleteConfiguration(undefined); - }} - /> - - ); -}; - -export default BiasConfigurationTable; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx deleted file mode 100644 index 9161f142d5..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/BiasConfigurationTableRow.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import * as React from 'react'; -import { ActionsColumn, ExpandableRowContent, Tbody, Td, Tr } from '@patternfly/react-table'; -import { - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, -} from '@patternfly/react-core'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; - -type BiasConfigurationTableRowProps = { - obj: BiasMetricConfig; - rowIndex: number; - onCloneConfiguration: (obj: BiasMetricConfig) => void; - onDeleteConfiguration: (obj: BiasMetricConfig) => void; -}; - -const BiasConfigurationTableRow: React.FC = ({ - obj, - rowIndex, - onCloneConfiguration, - onDeleteConfiguration, -}) => { - const [isExpanded, setExpanded] = React.useState(false); - - return ( - - - - - - - - - - - - - - - - ); -}; - -export default BiasConfigurationTableRow; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasMetricConfigSelector.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasMetricConfigSelector.tsx deleted file mode 100644 index 07e024b2f8..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/BiasMetricConfigSelector.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import { Select, SelectGroup, SelectOption, SelectVariant } from '@patternfly/react-core'; -import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; -import { BiasMetricType } from '~/api'; -import { - byId, - byNotId, - createBiasSelectOption, - isBiasSelectOption, -} from '~/pages/modelServing/screens/metrics/utils'; -import { BiasSelectOption } from '~/pages/modelServing/screens/metrics/types'; - -type BiasMetricConfigSelectorProps = { - onChange: (x: BiasMetricConfig[]) => void; - initialSelections: BiasMetricConfig[]; -}; - -const BiasMetricConfigSelector: React.FC = ({ - onChange, - initialSelections, -}) => { - const { biasMetricConfigs, loaded } = useExplainabilityModelData(); - - const [isOpen, setIsOpen] = React.useState(false); - - const selected = React.useMemo( - () => initialSelections.map(createBiasSelectOption), - [initialSelections], - ); - - const elementId = React.useId(); - - const changeState = React.useCallback( - (options: BiasSelectOption[]) => { - onChange(options.map((x) => x.biasMetricConfig)); - }, - [onChange], - ); - - return ( -
- - -
- ); -}; - -export default BiasMetricConfigSelector; diff --git a/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx b/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx deleted file mode 100644 index 94f176dbc2..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/BiasTab.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import { - Bullseye, - PageSection, - Spinner, - Stack, - StackItem, - ToolbarGroup, - ToolbarItem, -} from '@patternfly/react-core'; -import MetricsPageToolbar from '~/pages/modelServing/screens/metrics/MetricsPageToolbar'; -import BiasMetricConfigSelector from '~/pages/modelServing/screens/metrics/BiasMetricConfigSelector'; -import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; -import TrustyChart from '~/pages/modelServing/screens/metrics/TrustyChart'; -import EmptyBiasConfigurationCard from '~/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard'; -import EmptyBiasChartSelectionCard from '~/pages/modelServing/screens/metrics/EmptyBiasChartSelectionCard'; -import DashboardExpandableSection from '~/concepts/dashboard/DashboardExpandableSection'; -import useBiasChartsBrowserStorage from '~/pages/modelServing/screens/metrics/useBiasChartsBrowserStorage'; - -const OPEN_WRAPPER_STORAGE_KEY_PREFIX = `odh.dashboard.xai.bias_metric_chart_wrapper_open`; -const BiasTab: React.FC = () => { - const { biasMetricConfigs, loaded, loadError } = useExplainabilityModelData(); - - const [selectedBiasConfigs, setSelectedBiasConfigs] = useBiasChartsBrowserStorage(); - - const firstRender = React.useRef(true); - - React.useEffect(() => { - if (loaded && !loadError) { - if (firstRender.current) { - // If the user has just navigated here AND they haven't previously selected any charts to display, - // don't show them the "No selected" empty state, instead show them the first available chart. - // However, the user still needs to be shown said empty state if they deselect all charts. - firstRender.current = false; - if (selectedBiasConfigs.length === 0 && biasMetricConfigs.length > 0) { - // If biasMetricConfigs is empty, the "No Configured Metrics" empty state will be shown, so no need - // to set anything. - setSelectedBiasConfigs([biasMetricConfigs[0]]); - } - } - } - }, [loaded, biasMetricConfigs, setSelectedBiasConfigs, selectedBiasConfigs, loadError]); - - if (!loaded) { - return ( - - - - ); - } - - return ( - - - - - - Metrics to display - - - - - - - - - } - /> - - - - {(biasMetricConfigs.length === 0 && ( - - - - )) || - (selectedBiasConfigs.length === 0 && ( - - - - )) || ( - <> - {selectedBiasConfigs.map((x) => ( - - - - - - ))} - - )} - - - - ); -}; -export default BiasTab; diff --git a/frontend/src/pages/modelServing/screens/metrics/EmptyBiasChartSelectionCard.tsx b/frontend/src/pages/modelServing/screens/metrics/EmptyBiasChartSelectionCard.tsx deleted file mode 100644 index 1ee8f5b4e0..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/EmptyBiasChartSelectionCard.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { - Card, - CardBody, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - Title, -} from '@patternfly/react-core'; -import { SearchIcon } from '@patternfly/react-icons'; -import { - EMPTY_BIAS_CHART_SELECTION_DESC, - EMPTY_BIAS_CHART_SELECTION_TITLE, -} from '~/pages/modelServing/screens/metrics/const'; - -const EmptyBiasChartSelectionCard = () => ( - - - - - - {EMPTY_BIAS_CHART_SELECTION_TITLE} - - {EMPTY_BIAS_CHART_SELECTION_DESC} - - - -); - -export default EmptyBiasChartSelectionCard; diff --git a/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx b/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx deleted file mode 100644 index e43feefffc..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/EmptyBiasConfigurationCard.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { Button, Card, CardBody, EmptyStateVariant } from '@patternfly/react-core'; -import { useNavigate } from 'react-router-dom'; -import BiasConfigurationEmptyState from './BiasConfigurationEmptyState'; - -const EmptyBiasConfigurationCard: React.FC = () => { - const navigate = useNavigate(); - return ( - - - { - navigate('../configure', { relative: 'path' }); - }} - > - Configure - - } - variant={EmptyStateVariant.full} - /> - - - ); -}; - -export default EmptyBiasConfigurationCard; diff --git a/frontend/src/pages/modelServing/screens/metrics/GlobalModelMetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/GlobalModelMetricsPage.tsx deleted file mode 100644 index 4c8f45634f..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/GlobalModelMetricsPage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import { useOutletContext } from 'react-router-dom'; -import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; -import { PerformanceMetricType } from '~/pages/modelServing/screens/types'; -import MetricsPage from './MetricsPage'; -import { GlobalModelMetricsOutletContextProps } from './GlobalModelMetricsWrapper'; - -const GlobalModelMetricsPage: React.FC = () => { - const { model } = useOutletContext(); - const modelDisplayName = getInferenceServiceDisplayName(model); - return ( - - ); -}; - -export default GlobalModelMetricsPage; diff --git a/frontend/src/pages/modelServing/screens/metrics/GlobalModelMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/metrics/GlobalModelMetricsWrapper.tsx deleted file mode 100644 index 3e28e964e5..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/GlobalModelMetricsWrapper.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { Outlet } from 'react-router-dom'; -import { PerformanceMetricType } from '~/pages/modelServing/screens/types'; -import { InferenceServiceKind } from '~/k8sTypes'; -import ModelMetricsPathWrapper from './ModelMetricsPathWrapper'; -import { ModelServingMetricsProvider } from './ModelServingMetricsContext'; -import { getModelMetricsQueries } from './utils'; - -export type GlobalModelMetricsOutletContextProps = { - model: InferenceServiceKind; - projectName: string; -}; - -const GlobalModelMetricsWrapper: React.FC = () => ( - - {(model, projectName) => { - const queries = getModelMetricsQueries(model); - return ( - - - - ); - }} - -); - -export default GlobalModelMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx index 7086409624..6a4c5bbf1b 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx @@ -1,96 +1,54 @@ 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, getResizeObserver, } from '@patternfly/react-charts'; import { CubesIcon } from '@patternfly/react-icons'; -import { TimeframeTimeRange } from '~/pages/modelServing/screens/const'; +import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; +import { TimeframeTime } from '~/pages/modelServing/screens/const'; import { ModelServingMetricsContext } from './ModelServingMetricsContext'; -import { - DomainCalculator, - MetricChartLine, - MetricChartThreshold, - MetricsChartTypes, - ProcessedMetrics, -} from './types'; -import { - convertTimestamp, - createGraphMetricLine, - defaultDomainCalculator, - formatToShow, - getThresholdData, - useStableMetrics, -} from './utils'; +import { convertTimestamp, formatToShow, getThresholdData } from './utils'; type MetricsChartProps = { title: string; - color?: string; - metrics: MetricChartLine; - thresholds?: MetricChartThreshold[]; - domain?: DomainCalculator; - toolbar?: React.ReactElement; - type?: MetricsChartTypes; + color: string; + metrics: ContextResourceData; + unit?: string; + threshold?: number; }; -const MetricsChart: React.FC = ({ - title, - color, - metrics: unstableMetrics, - thresholds = [], - domain = defaultDomainCalculator, - toolbar, - type = MetricsChartTypes.AREA, -}) => { + +const MetricsChart: React.FC = ({ title, color, metrics, unit, threshold }) => { 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, - minYValue, - } = React.useMemo( + const processedData = 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, minYValue: 0 }, - ), - [metrics], + metrics.data?.map((data) => ({ + x: data[0] * 1000, + y: parseInt(data[1]), + name: title, + })) || [], + [metrics, title], ); - const error = metrics.find((line) => line.metric.error)?.metric.error; - const isAllLoaded = metrics.every((line) => line.metric.loaded); - const hasSomeData = graphLines.some((line) => line.length > 0); + const maxValue = Math.max(...processedData.map((e) => e.y)); + + const hasData = processedData.length > 0; React.useEffect(() => { const ref = bodyRef.current; @@ -103,31 +61,14 @@ const MetricsChart: React.FC = ({ handleResize(); } return () => observer(); - }, []); - - let legendProps: Partial> = {}; - if (metrics.length > 1 && metrics.every(({ name }) => !!name)) { - // We don't need a label if there is only one line & we need a name for every item (or it won't align) - legendProps = { - legendData: metrics.map(({ name }) => ({ name })), - legendOrientation: 'horizontal', - legendPosition: 'bottom-left', - }; - } + }, [bodyRef]); return ( - - {title} - {toolbar && ( - - {toolbar} - - )} - - + {`${title}${unit ? ` (${unit})` : ''}`} +
- {hasSomeData ? ( + {hasData ? ( = ({ constrainToVisibleArea /> } - domain={domain(maxYValue, minYValue)} + domain={{ y: maxValue === 0 ? [-1, 1] : [0, maxValue + 1] }} height={400} width={chartWidth} padding={{ left: 70, right: 50, bottom: 70, top: 50 }} - themeColor={color ?? ChartThemeColor.multi} - {...legendProps} + themeColor={color} > convertTimestamp(x, formatToShow(currentTimeframe))} - tickValues={[]} domain={{ - x: [lastUpdateTime - TimeframeTimeRange[currentTimeframe] * 1000, lastUpdateTime], + x: [lastUpdateTime - TimeframeTime[currentTimeframe] * 1000, lastUpdateTime], }} fixLabelOverlap /> - {graphLines.map((line, i) => { - switch (type) { - case MetricsChartTypes.AREA: - return ; - break; - case MetricsChartTypes.LINE: - return ; - break; - default: - return null; - } - })} + - {thresholds.map((t) => ( - - ))} + {threshold && } ) : ( - {isAllLoaded ? ( + {metrics.loaded ? ( <> - {error ? error.message : 'No available data'} + {metrics.error ? metrics.error.message : 'No available data'} ) : ( diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx index 19e4a8fe71..701537033d 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx @@ -1,53 +1,80 @@ import * as React from 'react'; -import { Breadcrumb, Button } from '@patternfly/react-core'; -import { useNavigate, useParams } from 'react-router-dom'; -import { CogIcon } from '@patternfly/react-icons'; +import { Breadcrumb, BreadcrumbItem, PageSection, Stack, StackItem } from '@patternfly/react-core'; +import { Link } from 'react-router-dom'; import { BreadcrumbItemType } from '~/types'; import ApplicationsPage from '~/pages/ApplicationsPage'; -import MetricsPageTabs from '~/pages/modelServing/screens/metrics/MetricsPageTabs'; -import { MetricsTabKeys } from '~/pages/modelServing/screens/metrics/types'; -import { PerformanceMetricType } from '~/pages/modelServing/screens/types'; -import { ExplainabilityContext } from '~/concepts/explainability/ExplainabilityContext'; -import { getBreadcrumbItemComponents } from './utils'; -import PerformanceTab from './PerformanceTab'; +import MetricsChart from './MetricsChart'; +import MetricsPageToolbar from './MetricsPageToolbar'; +import { ModelServingMetricsContext, ModelServingMetricType } from './ModelServingMetricsContext'; type MetricsPageProps = { title: string; breadcrumbItems: BreadcrumbItemType[]; - type: PerformanceMetricType; }; -const MetricsPage: React.FC = ({ title, breadcrumbItems, type }) => { - const { tab } = useParams(); - const navigate = useNavigate(); - - const { - hasCR, - apiState: { apiAvailable }, - } = React.useContext(ExplainabilityContext); - +const MetricsPage: React.FC = ({ title, breadcrumbItems }) => { + const { data } = React.useContext(ModelServingMetricsContext); return ( {getBreadcrumbItemComponents(breadcrumbItems)}} - // TODO: decide whether we need to set the loaded based on the feature flag and explainability loaded + breadcrumb={ + + {breadcrumbItems.map((item) => ( + + item.link ? {item.label} : <>{item.label} + } + /> + ))} + + } + toolbar={} loaded description={null} empty={false} - headerAction={ - tab === MetricsTabKeys.BIAS && ( - - ) - } > - {type === PerformanceMetricType.SERVER ? : } + + + + + + + + + + + + + + + + + + + ); }; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.scss b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.scss deleted file mode 100644 index ebd929f92a..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.scss +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index bc61a1914c..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { Tab, TabAction, Tabs, TabTitleText } from '@patternfly/react-core'; -import { MetricsTabKeys } from '~/pages/modelServing/screens/metrics/types'; -import { useExplainabilityModelData } from '~/concepts/explainability/useExplainabilityModelData'; -import NotFound from '~/pages/NotFound'; -import useBiasMetricsInstalled from '~/concepts/explainability/useBiasMetricsInstalled'; -import PerformanceTab from './PerformanceTab'; -import BiasTab from './BiasTab'; -import BiasConfigurationAlertPopover from './BiasConfigurationAlertPopover'; -import useMetricsPageEnabledTabs from './useMetricsPageEnabledTabs'; -import usePerformanceMetricsEnabled from './usePerformanceMetricsEnabled'; - -import './MetricsPageTabs.scss'; - -const MetricsPageTabs: React.FC = () => { - const enabledTabs = useMetricsPageEnabledTabs(); - const { biasMetricConfigs, loaded } = useExplainabilityModelData(); - const [biasMetricsInstalled] = useBiasMetricsInstalled(); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); - const { tab } = useParams<{ tab: MetricsTabKeys }>(); - const navigate = useNavigate(); - - React.useEffect(() => { - if (!tab) { - navigate(`./${enabledTabs[0]}`, { replace: true }); - } else if (!enabledTabs.includes(tab)) { - navigate(`../${enabledTabs[0]}`, { replace: true }); - } - }, [enabledTabs, navigate, tab]); - - if (enabledTabs.length === 0) { - return ; - } - - return ( - { - if (typeof tabId === 'string') { - navigate(`../${tabId}`, { relative: 'path' }); - } - }} - isBox={false} - aria-label="Metrics page tabs" - role="region" - className="odh-tabs-fix" - > - {performanceMetricsEnabled && ( - Endpoint Performance} - aria-label="Performance tab" - className="odh-tabcontent-fix" - > - - - )} - {biasMetricsInstalled && ( - Model Bias} - aria-label="Bias tab" - className="odh-tabcontent-fix" - actions={ - loaded && - biasMetricConfigs.length === 0 && ( - - { - navigate('../configure', { relative: 'path' }); - }} - /> - - ) - } - > - - - )} - - ); -}; - -export default MetricsPageTabs; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx index 095710ee03..9ece1bc5e3 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPageToolbar.tsx @@ -1,87 +1,54 @@ import * as React from 'react'; import { + Button, Select, SelectOption, - Stack, - StackItem, + Text, Toolbar, ToolbarContent, - ToolbarGroup, ToolbarItem, } from '@patternfly/react-core'; -import { RefreshIntervalTitle, TimeframeTitle } from '~/pages/modelServing/screens/types'; -import { isRefreshIntervalTitle, isTimeframeTitle } from './utils'; +import { SyncAltIcon } from '@patternfly/react-icons'; +import { TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { relativeTime } from '~/utilities/time'; +import { isTimeframeTitle } from './utils'; import { ModelServingMetricsContext } from './ModelServingMetricsContext'; -type MetricsPageToolbarProps = { - leftToolbarItem?: React.ReactElement; -}; - -const MetricsPageToolbar: React.FC = ({ leftToolbarItem }) => { +const MetricsPageToolbar: React.FC = () => { const [timeframeOpen, setTimeframeOpen] = React.useState(false); - const { - currentTimeframe, - setCurrentTimeframe, - currentRefreshInterval, - setCurrentRefreshInterval, - } = React.useContext(ModelServingMetricsContext); - - const [intervalOpen, setIntervalOpen] = React.useState(false); - + const { currentTimeframe, setCurrentTimeframe, refresh, lastUpdateTime } = React.useContext( + ModelServingMetricsContext, + ); return ( - + - {leftToolbarItem} - - - - - Time range - - - - - - - - - - Refresh interval - - - - - - - + + + + + + + + Last update +
+ {relativeTime(Date.now(), lastUpdateTime)} +
); diff --git a/frontend/src/pages/modelServing/screens/metrics/ModelGraphs.tsx b/frontend/src/pages/modelServing/screens/metrics/ModelGraphs.tsx deleted file mode 100644 index a7426535c2..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/ModelGraphs.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react'; -import { Stack, StackItem } from '@patternfly/react-core'; -import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; -import { - ModelMetricType, - ModelServingMetricsContext, -} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { TimeframeTitle } from '~/pages/modelServing/screens/types'; -import { per100 } from './utils'; - -const ModelGraphs: React.FC = () => { - const { data, currentTimeframe } = React.useContext(ModelServingMetricsContext); - - const inHours = - currentTimeframe === TimeframeTitle.ONE_HOUR || currentTimeframe === TimeframeTitle.ONE_DAY; - - return ( - - - - - - ); -}; - -export default ModelGraphs; diff --git a/frontend/src/pages/modelServing/screens/metrics/ModelMetricsPathWrapper.tsx b/frontend/src/pages/modelServing/screens/metrics/ModelMetricsPathWrapper.tsx deleted file mode 100644 index 9b16e1c7d4..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/ModelMetricsPathWrapper.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from 'react'; -import { useParams } from 'react-router-dom'; -import { Bullseye, Spinner } from '@patternfly/react-core'; -import { ModelServingContext } from '~/pages/modelServing/ModelServingContext'; -import NotFound from '~/pages/NotFound'; -import { InferenceServiceKind } from '~/k8sTypes'; - -type ModelMetricsPathWrapperProps = { - children: (inferenceService: InferenceServiceKind, projectName: string) => React.ReactNode; -}; - -const ModelMetricsPathWrapper: React.FC = ({ children }) => { - const { project: projectName, inferenceService: modelName } = useParams<{ - project: string; - inferenceService: string; - }>(); - const { - inferenceServices: { data: models, loaded }, - } = React.useContext(ModelServingContext); - const inferenceService = models.find( - (model) => model.metadata.name === modelName && model.metadata.namespace === projectName, - ); - if (!loaded) { - return ( - - - - ); - } - if (!inferenceService || !projectName) { - return ; - } - - return <>{children(inferenceService, projectName)}; -}; - -export default ModelMetricsPathWrapper; diff --git a/frontend/src/pages/modelServing/screens/metrics/ModelServingExplainabilityWrapper.tsx b/frontend/src/pages/modelServing/screens/metrics/ModelServingExplainabilityWrapper.tsx deleted file mode 100644 index aa3c84d4e5..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/ModelServingExplainabilityWrapper.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { Outlet, useParams } from 'react-router-dom'; -import { ExplainabilityContextProvider } from '~/concepts/explainability/ExplainabilityContext'; -import NotFound from '~/pages/NotFound'; - -const ModelServingExplainabilityWrapper: React.FC = () => { - const { project: namespace } = useParams<{ project: string }>(); - - if (!namespace) { - return ; - } - - return ( - - - - ); -}; - -export default ModelServingExplainabilityWrapper; diff --git a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx b/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx index 1e6e898408..68ab5ac7dc 100644 --- a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx @@ -2,35 +2,20 @@ import * as React from 'react'; import { useModelServingMetrics } from '~/api'; import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; import { DEFAULT_CONTEXT_DATA } from '~/utilities/const'; -import { - PerformanceMetricType, - RefreshIntervalTitle, - TimeframeTitle, -} from '~/pages/modelServing/screens/types'; +import { TimeframeTitle } from '~/pages/modelServing/screens/types'; -export enum ServerMetricType { - AVG_RESPONSE_TIME = 'runtime_avg-response-time', - REQUEST_COUNT = 'runtime_requests-count', - CPU_UTILIZATION = 'runtime_cpu-utilization', - MEMORY_UTILIZATION = 'runtime_memory-utilization', -} - -export enum ModelMetricType { - REQUEST_COUNT_SUCCESS = 'inference_request-count-successes', - REQUEST_COUNT_FAILED = 'inference_request-count-fails', - TRUSTY_AI_SPD = 'trustyai_spd', - TRUSTY_AI_DIR = 'trustyai_dir', +export enum ModelServingMetricType { + ENDPOINT_HEALTH = 'end-point-health', + INFERENCE_PERFORMANCE = 'inference-performance', + AVG_RESPONSE_TIME = 'avg-response-time', + REQUEST_COUNT = 'request-count', + FAILED_REQUEST_COUNT = 'failed-request-count', } type ModelServingMetricsContext = { - data: Record< - ModelMetricType & ServerMetricType, - ContextResourceData - >; + data: Record>; currentTimeframe: TimeframeTitle; setCurrentTimeframe: (timeframe: TimeframeTitle) => void; - currentRefreshInterval: RefreshIntervalTitle; - setCurrentRefreshInterval: (interval: RefreshIntervalTitle) => void; refresh: () => void; lastUpdateTime: number; setLastUpdateTime: (time: number) => void; @@ -38,19 +23,14 @@ type ModelServingMetricsContext = { export const ModelServingMetricsContext = React.createContext({ data: { - [ServerMetricType.REQUEST_COUNT]: DEFAULT_CONTEXT_DATA, - [ServerMetricType.AVG_RESPONSE_TIME]: DEFAULT_CONTEXT_DATA, - [ServerMetricType.CPU_UTILIZATION]: DEFAULT_CONTEXT_DATA, - [ServerMetricType.MEMORY_UTILIZATION]: DEFAULT_CONTEXT_DATA, - [ModelMetricType.REQUEST_COUNT_FAILED]: DEFAULT_CONTEXT_DATA, - [ModelMetricType.REQUEST_COUNT_SUCCESS]: DEFAULT_CONTEXT_DATA, - [ModelMetricType.TRUSTY_AI_SPD]: DEFAULT_CONTEXT_DATA, - [ModelMetricType.TRUSTY_AI_DIR]: DEFAULT_CONTEXT_DATA, + [ModelServingMetricType.ENDPOINT_HEALTH]: DEFAULT_CONTEXT_DATA, + [ModelServingMetricType.INFERENCE_PERFORMANCE]: DEFAULT_CONTEXT_DATA, + [ModelServingMetricType.AVG_RESPONSE_TIME]: DEFAULT_CONTEXT_DATA, + [ModelServingMetricType.REQUEST_COUNT]: DEFAULT_CONTEXT_DATA, + [ModelServingMetricType.FAILED_REQUEST_COUNT]: DEFAULT_CONTEXT_DATA, }, - currentTimeframe: TimeframeTitle.ONE_HOUR, + currentTimeframe: TimeframeTitle.FIVE_MINUTES, setCurrentTimeframe: () => undefined, - currentRefreshInterval: RefreshIntervalTitle.FIVE_MINUTES, - setCurrentRefreshInterval: () => undefined, refresh: () => undefined, lastUpdateTime: 0, setLastUpdateTime: () => undefined, @@ -59,31 +39,23 @@ export const ModelServingMetricsContext = React.createContext | Record; - type: PerformanceMetricType; + queries: Record; }; export const ModelServingMetricsProvider: React.FC = ({ queries, children, - type, }) => { const [currentTimeframe, setCurrentTimeframe] = React.useState( TimeframeTitle.ONE_DAY, ); - - const [currentRefreshInterval, setCurrentRefreshInterval] = React.useState( - RefreshIntervalTitle.FIVE_MINUTES, - ); const [lastUpdateTime, setLastUpdateTime] = React.useState(Date.now()); const { data, refresh } = useModelServingMetrics( - type, queries, currentTimeframe, lastUpdateTime, setLastUpdateTime, - currentRefreshInterval, ); return ( @@ -92,8 +64,6 @@ export const ModelServingMetricsProvider: React.FC { + const { project: projectName, inferenceService: modelName } = useParams<{ + project: string; + inferenceService: string; + }>(); + const { + inferenceServices: { data: models, loaded }, + } = React.useContext(ModelServingContext); + const inferenceService = models.find( + (model) => model.metadata.name === modelName && model.metadata.namespace === projectName, + ); + if (!loaded) { + return ( + + + + ); + } + if (!inferenceService) { + return ; + } + const queries = getInferenceServiceMetricsQueries(inferenceService); + const modelDisplayName = getInferenceServiceDisplayName(inferenceService); + + return ( + + + + ); +}; + +export default ModelServingMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/metrics/PerformanceTab.tsx b/frontend/src/pages/modelServing/screens/metrics/PerformanceTab.tsx deleted file mode 100644 index 0a7b3dae8f..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/PerformanceTab.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { PageSection, Stack, StackItem } from '@patternfly/react-core'; -import ModelGraphs from '~/pages/modelServing/screens/metrics/ModelGraphs'; -import MetricsPageToolbar from '~/pages/modelServing/screens/metrics/MetricsPageToolbar'; -import { PerformanceMetricType } from '~/pages/modelServing/screens/types'; -import ServerGraphs from './ServerGraphs'; - -type PerformanceTabProps = { - type?: PerformanceMetricType; -}; - -const PerformanceTab: React.FC = ({ type }) => ( - - - - - - {type === PerformanceMetricType.SERVER ? : } - - -); - -export default PerformanceTab; diff --git a/frontend/src/pages/modelServing/screens/metrics/ServerGraphs.tsx b/frontend/src/pages/modelServing/screens/metrics/ServerGraphs.tsx deleted file mode 100644 index deb05ddeb2..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/ServerGraphs.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from 'react'; -import { Stack, StackItem } from '@patternfly/react-core'; -import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; -import { - ModelServingMetricsContext, - ServerMetricType, -} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { TimeframeTitle } from '~/pages/modelServing/screens/types'; -import { per100 } from '~/pages/modelServing/screens/metrics/utils'; - -const ServerGraphs: React.FC = () => { - const { data, currentTimeframe } = React.useContext(ModelServingMetricsContext); - - const inHours = - currentTimeframe === TimeframeTitle.ONE_HOUR || currentTimeframe === TimeframeTitle.ONE_DAY; - - return ( - - - - - - - - - - - - - - - ); -}; - -export default ServerGraphs; diff --git a/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx b/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx deleted file mode 100644 index 72aa4db4ae..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/TrustyChart.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; -import { ModelServingMetricsContext } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; -import { createChartThresholds } from '~/pages/modelServing/screens/metrics/utils'; -import { BIAS_CHART_CONFIGS } from '~/pages/modelServing/screens/metrics/const'; - -export type TrustyChartProps = { - biasMetricConfig: BiasMetricConfig; -}; - -const TrustyChart: React.FC = ({ biasMetricConfig }) => { - const { data } = React.useContext(ModelServingMetricsContext); - - const { id, metricType, thresholdDelta } = biasMetricConfig; - - const { title, abbreviation, modelMetricKey, chartType, domainCalculator } = - BIAS_CHART_CONFIGS[metricType]; - - const metric = React.useMemo(() => { - const metricData = data[modelMetricKey].data; - - const values = metricData.find((x) => x.metric.request === id)?.values; - - return { - ...data[modelMetricKey], - data: values, - }; - }, [data, id, modelMetricKey]); - - return ( - - ); -}; - -export default TrustyChart; diff --git a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal.tsx b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal.tsx deleted file mode 100644 index 0aeec8d8d7..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/DeleteBiasConfigurationModal.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from 'react'; -import { BiasMetricType } from '~/api'; -import { ExplainabilityContext } from '~/concepts/explainability/ExplainabilityContext'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; -import DeleteModal from '~/pages/projects/components/DeleteModal'; -import useBiasChartsBrowserStorage from '~/pages/modelServing/screens/metrics/useBiasChartsBrowserStorage'; -import { byNotId } from '~/pages/modelServing/screens/metrics/utils'; - -type DeleteBiasConfigurationModalProps = { - configurationToDelete?: BiasMetricConfig; - onClose: (deleted: boolean) => void; -}; - -const DeleteBiasConfigurationModal: React.FC = ({ - onClose, - configurationToDelete, -}) => { - const [isDeleting, setDeleting] = React.useState(false); - const [error, setError] = React.useState(); - const { - apiState: { api }, - } = React.useContext(ExplainabilityContext); - - const [selectedBiasConfigCharts, setSelectedBiasConfigCharts] = useBiasChartsBrowserStorage(); - - const deselectDeleted = (biasConfigId: string) => { - setSelectedBiasConfigCharts(selectedBiasConfigCharts.filter(byNotId(biasConfigId))); - }; - - const onBeforeClose = (deleted: boolean) => { - onClose(deleted); - setDeleting(false); - setError(undefined); - }; - - const displayName = configurationToDelete ? configurationToDelete.name : 'this bias metric'; - return ( - onBeforeClose(false)} - submitButtonLabel="Delete bias metric" - onDelete={() => { - if (configurationToDelete) { - setDeleting(true); - const deleteFunc = - configurationToDelete.metricType === BiasMetricType.DIR - ? api.deleteDirRequest - : api.deleteSpdRequest; - deleteFunc({}, configurationToDelete.id) - .then(() => onBeforeClose(true)) - .then(() => deselectDeleted(configurationToDelete.id)) - .catch((e) => { - setError(e); - setDeleting(false); - }); - } - }} - deleting={isDeleting} - error={error} - deleteName={displayName} - > - This action cannot be undone. - - ); -}; - -export default DeleteBiasConfigurationModal; diff --git a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx deleted file mode 100644 index 4b23cbab67..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/ManageBiasConfigurationModal.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import * as React from 'react'; -import { Form, FormGroup, Modal, TextInput } from '@patternfly/react-core'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; -import { BiasMetricType } from '~/api'; -import { InferenceServiceKind } from '~/k8sTypes'; -import { ExplainabilityContext } from '~/concepts/explainability/ExplainabilityContext'; -import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; -import { - checkConfigurationFieldsValid, - convertConfigurationRequestType, - getThresholdDefaultDelta, -} from '~/pages/modelServing/screens/metrics/utils'; -import DashboardHelpTooltip from '~/concepts/dashboard/DashboardHelpTooltip'; -import useBiasConfigurationObject from './useBiasConfigurationObject'; -import MetricTypeField from './MetricTypeField'; - -type ManageBiasConfigurationModalProps = { - existingConfiguration?: BiasMetricConfig; - isOpen: boolean; - onClose: (submit: boolean) => void; - inferenceService: InferenceServiceKind; -}; - -const ManageBiasConfigurationModal: React.FC = ({ - existingConfiguration, - isOpen, - onClose, - inferenceService, -}) => { - const { - apiState: { api }, - } = React.useContext(ExplainabilityContext); - const [actionInProgress, setActionInProgress] = React.useState(false); - const [error, setError] = React.useState(); - const [metricType, setMetricType] = React.useState(); - const [configuration, setConfiguration, resetData] = useBiasConfigurationObject( - inferenceService.metadata.name, - metricType, - existingConfiguration, - ); - - React.useEffect(() => { - setMetricType(existingConfiguration?.metricType); - }, [existingConfiguration]); - - const onBeforeClose = (submitted: boolean) => { - onClose(submitted); - setError(undefined); - setActionInProgress(false); - resetData(); - setMetricType(undefined); - }; - - const onCreateConfiguration = () => { - const createFunc = - metricType === BiasMetricType.SPD ? api.createSpdRequest : api.createDirRequest; - setActionInProgress(true); - createFunc({}, convertConfigurationRequestType(configuration)) - .then(() => onBeforeClose(true)) - .catch((e) => { - setError(e); - setActionInProgress(false); - }); - }; - - return ( - onBeforeClose(false)} - footer={ - onBeforeClose(false)} - onSubmit={onCreateConfiguration} - submitLabel="Configure" - alertTitle="Failed to configure bias metric" - isSubmitDisabled={ - !checkConfigurationFieldsValid(configuration, metricType) || actionInProgress - } - /> - } - description="All fields are required." - > -
- - setConfiguration('requestName', value)} - /> - - { - setMetricType(value); - setConfiguration('thresholdDelta', getThresholdDefaultDelta(value)); - }} - /> - - } - > - setConfiguration('protectedAttribute', value)} - /> - - - } - > - setConfiguration('privilegedAttribute', value)} - /> - - - } - > - setConfiguration('unprivilegedAttribute', value)} - /> - - - } - > - setConfiguration('outcomeName', value)} - /> - - - } - > - setConfiguration('favorableOutcome', value)} - /> - - - } - > - setConfiguration('thresholdDelta', Number(value))} - /> - - - } - > - setConfiguration('batchSize', Number(value))} - /> - - -
- ); -}; - -export default ManageBiasConfigurationModal; diff --git a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/MetricTypeField.tsx b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/MetricTypeField.tsx deleted file mode 100644 index 454bd81f3f..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/MetricTypeField.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { FormGroup, Select, SelectOption } from '@patternfly/react-core'; -import { - METRIC_TYPE_DESCRIPTION, - METRIC_TYPE_DISPLAY_NAME, -} from '~/pages/modelServing/screens/metrics/const'; -import { BiasMetricType } from '~/api'; -import { isMetricType } from '~/pages/modelServing/screens/metrics/utils'; - -type MetricTypeFieldProps = { - fieldId: string; - value?: BiasMetricType; - onChange: (value: BiasMetricType) => void; -}; - -const MetricTypeField: React.FC = ({ fieldId, value, onChange }) => { - const [isOpen, setOpen] = React.useState(false); - return ( - - - - ); -}; - -export default MetricTypeField; diff --git a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/useBiasConfigurationObject.ts b/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/useBiasConfigurationObject.ts deleted file mode 100644 index 03d631ab2e..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/biasConfigurationModal/useBiasConfigurationObject.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from 'react'; -import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; -import useGenericObjectState from '~/utilities/useGenericObjectState'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; -import { BaseMetricRequestInput, BiasMetricType } from '~/api'; -import { getThresholdDefaultDelta } from '~/pages/modelServing/screens/metrics/utils'; - -const useBiasConfigurationObject = ( - modelId: string, - metricType?: BiasMetricType, - existingData?: BiasMetricConfig, -): [ - data: BaseMetricRequestInput, - setData: UpdateObjectAtPropAndValue, - resetDefaults: () => void, -] => { - const createConfiguration = useGenericObjectState({ - modelId: modelId, - requestName: '', - protectedAttribute: '', - privilegedAttribute: '', - unprivilegedAttribute: '', - outcomeName: '', - favorableOutcome: '', - thresholdDelta: undefined, - batchSize: 5000, - }); - - const [, setCreateData] = createConfiguration; - - const existingModelId = existingData?.modelId ?? modelId; - const existingName = existingData?.name ? `Copy of ${existingData.name}` : ''; - const existingProtectedAttribute = existingData?.protectedAttribute ?? ''; - const existingPrivilegedAttribute = existingData?.privilegedAttribute ?? ''; - const existingUnprivilegedAttribute = existingData?.unprivilegedAttribute ?? ''; - const existingOutcomeName = existingData?.outcomeName ?? ''; - const existingFavorableOutcome = existingData?.favorableOutcome ?? ''; - const existingThresholdDelta = - existingData?.thresholdDelta ?? getThresholdDefaultDelta(metricType); - const existingBatchSize = existingData?.batchSize ?? 5000; - - React.useEffect(() => { - if (existingData) { - setCreateData('modelId', existingModelId); - setCreateData('requestName', existingName); - setCreateData('protectedAttribute', existingProtectedAttribute); - setCreateData('privilegedAttribute', existingPrivilegedAttribute); - setCreateData('unprivilegedAttribute', existingUnprivilegedAttribute); - setCreateData('outcomeName', existingOutcomeName); - setCreateData('favorableOutcome', existingFavorableOutcome); - setCreateData('thresholdDelta', existingThresholdDelta); - setCreateData('batchSize', existingBatchSize); - } - }, [ - setCreateData, - existingData, - existingModelId, - existingName, - existingProtectedAttribute, - existingPrivilegedAttribute, - existingUnprivilegedAttribute, - existingOutcomeName, - existingFavorableOutcome, - existingThresholdDelta, - existingBatchSize, - ]); - - return createConfiguration; -}; - -export default useBiasConfigurationObject; diff --git a/frontend/src/pages/modelServing/screens/metrics/const.ts b/frontend/src/pages/modelServing/screens/metrics/const.ts deleted file mode 100644 index fc3a3d80f6..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/const.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { BiasMetricType } from '~/api'; -import { BiasChartConfigMap, MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; -import { ModelMetricType } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { calculateThresholds } from '~/pages/modelServing/screens/metrics/utils'; - -export const EMPTY_BIAS_CONFIGURATION_TITLE = 'Bias metrics not configured'; -export const EMPTY_BIAS_CONFIGURATION_DESC = - 'Bias metrics for this model have not been configured. To monitor model bias, you must first configure metrics.'; - -export const METRIC_TYPE_DISPLAY_NAME: { [key in BiasMetricType]: string } = { - [BiasMetricType.DIR]: 'Disparate impact ratio (DIR)', - [BiasMetricType.SPD]: 'Statistical parity difference (SPD)', -}; - -export const METRIC_TYPE_DESCRIPTION: { [key in BiasMetricType]: string } = { - [BiasMetricType.DIR]: - 'Calculates the ratio between the proportion of the privileged and unprivileged groups getting a particular outcome.', - [BiasMetricType.SPD]: - 'Calculates the difference between the proportion of the privileged and unprivileged groups getting a particular outcome.', -}; - -export const EMPTY_BIAS_CHART_SELECTION_TITLE = 'No Bias metrics selected'; -export const EMPTY_BIAS_CHART_SELECTION_DESC = - 'No bias metrics have been selected. To display charts you must first select them using the metric selector.'; - -export const BIAS_THRESHOLD_COLOR = 'red'; -export const BIAS_DOMAIN_PADDING = 0.1; -export const DEFAULT_BIAS_THRESHOLD_DELTAS: { [key in BiasMetricType]: number } = { - [BiasMetricType.SPD]: 0.1, - [BiasMetricType.DIR]: 0.2, -}; - -export const BIAS_CHART_CONFIGS: BiasChartConfigMap = { - [BiasMetricType.SPD]: { - title: 'Statistical Parity Difference', - abbreviation: 'SPD', - modelMetricKey: ModelMetricType.TRUSTY_AI_SPD, - chartType: MetricsChartTypes.AREA, - thresholdOrigin: 0, - defaultDelta: 0.1, - domainCalculator: (delta) => (maxYValue, minYValue) => { - const { thresholdOrigin, defaultDelta } = BIAS_CHART_CONFIGS[BiasMetricType.SPD]; - - const [maxThreshold, minThreshold] = calculateThresholds( - thresholdOrigin, - delta ?? defaultDelta, - ); - - const max = Math.max(Math.abs(maxYValue), Math.abs(minYValue)); - - return { - y: - max > maxThreshold - ? [-1 * max - BIAS_DOMAIN_PADDING, max + BIAS_DOMAIN_PADDING] - : [minThreshold - BIAS_DOMAIN_PADDING, maxThreshold + BIAS_DOMAIN_PADDING], - }; - }, - }, - [BiasMetricType.DIR]: { - title: 'Disparate Impact Ratio', - abbreviation: 'DIR', - modelMetricKey: ModelMetricType.TRUSTY_AI_DIR, - chartType: MetricsChartTypes.LINE, - thresholdOrigin: 1, - defaultDelta: 0.2, - domainCalculator: (delta) => (maxYValue) => { - const { thresholdOrigin, defaultDelta } = BIAS_CHART_CONFIGS[BiasMetricType.DIR]; - const [maxThreshold] = calculateThresholds(thresholdOrigin, delta ?? defaultDelta); - - return { - y: - maxYValue > maxThreshold - ? [0, maxYValue + BIAS_DOMAIN_PADDING] - : [0, maxThreshold + BIAS_DOMAIN_PADDING], - }; - }, - }, -}; diff --git a/frontend/src/pages/modelServing/screens/metrics/tableData.tsx b/frontend/src/pages/modelServing/screens/metrics/tableData.tsx deleted file mode 100644 index 2c44a6d0a4..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/tableData.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { SortableData } from '~/components/table/useTableColumnSort'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; - -export const columns: SortableData[] = [ - { - field: 'expand', - label: '', - sortable: false, - }, - { - field: 'name', - label: 'Name', - sortable: (a, b) => a.name.localeCompare(b.name), - }, - { - field: 'metric', - label: 'Metric', - sortable: false, - }, - { - field: 'protected-attribute', - label: 'Protected attribute', - sortable: false, - }, - { - field: 'privileged-value', - label: 'Privileged value', - sortable: false, - }, - { - field: 'unprivileged-value', - label: 'Unprivileged value', - sortable: false, - }, - { - field: 'output', - label: 'Output', - sortable: false, - }, - { - field: 'output-value', - label: 'Output value', - sortable: false, - }, - { - field: 'kebab', - label: '', - sortable: false, - }, -]; diff --git a/frontend/src/pages/modelServing/screens/metrics/types.ts b/frontend/src/pages/modelServing/screens/metrics/types.ts deleted file mode 100644 index ce09a759fa..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/types.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { DomainTuple, ForAxes } from 'victory-core'; -import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; -import { BiasMetricType } from '~/api'; -import { ModelMetricType } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; - -export type TranslatePoint = (line: GraphMetricPoint) => GraphMetricPoint; - -type MetricChartLineBase = { - metric: ContextResourceData; - translatePoint?: TranslatePoint; -}; -export type NamedMetricChartLine = MetricChartLineBase & { - name: string; -}; -export type UnnamedMetricChartLine = MetricChartLineBase & { - /** Assumes chart title */ - name?: string; -}; -export type MetricChartLine = UnnamedMetricChartLine | NamedMetricChartLine[]; - -export type GraphMetricPoint = { - x: number; - y: number; - name: string; -}; -export type GraphMetricLine = GraphMetricPoint[]; - -export type ProcessedMetrics = { - data: GraphMetricLine[]; - maxYValue: number; - minYValue: number; -}; - -export type MetricChartThreshold = { - value: number; - color?: string; - label?: string; -}; - -export type DomainCalculator = (maxYValue: number, minYValue: number) => ForAxes; - -export enum MetricsChartTypes { - AREA, - LINE, -} - -export enum MetricsTabKeys { - PERFORMANCE = 'performance', - BIAS = 'bias', -} - -export type BiasChartConfig = { - title: string; - abbreviation: string; - domainCalculator: (userDelta: number | undefined) => DomainCalculator; - modelMetricKey: ModelMetricType; - chartType: MetricsChartTypes; - thresholdOrigin: number; - defaultDelta: number; -}; -export type BiasChartConfigMap = { [key in BiasMetricType]: BiasChartConfig }; - -export type BiasSelectOption = { - id: string; - name: string; - biasMetricConfig: BiasMetricConfig; - toString: () => string; - compareTo: (x: BiasSelectOption) => boolean; -}; diff --git a/frontend/src/pages/modelServing/screens/metrics/useBiasChartsBrowserStorage.ts b/frontend/src/pages/modelServing/screens/metrics/useBiasChartsBrowserStorage.ts deleted file mode 100644 index 94a1a18326..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/useBiasChartsBrowserStorage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useParams } from 'react-router-dom'; -import { useBrowserStorage } from '~/components/browserStorage'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; -import { SetBrowserStorageHook } from '~/components/browserStorage/BrowserStorageContext'; - -const SELECTED_CHARTS_STORAGE_KEY_PREFIX = 'odh.dashboard.xai.selected_bias_charts'; - -const useBiasChartsBrowserStorage = (): [ - BiasMetricConfig[], - SetBrowserStorageHook, -] => { - const { inferenceService } = useParams(); - - const [selectedBiasConfigs, setSelectedBiasConfigs] = useBrowserStorage( - `${SELECTED_CHARTS_STORAGE_KEY_PREFIX}-${inferenceService}`, - [], - true, - true, - ); - - return [selectedBiasConfigs, setSelectedBiasConfigs]; -}; - -export default useBiasChartsBrowserStorage; diff --git a/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts b/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts deleted file mode 100644 index 124b59ba1d..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts +++ /dev/null @@ -1,18 +0,0 @@ -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; -import { MetricsTabKeys } from './types'; -import usePerformanceMetricsEnabled from './usePerformanceMetricsEnabled'; - -const useMetricsPageEnabledTabs = () => { - const enabledTabs: MetricsTabKeys[] = []; - const [biasMetricsEnabled] = useBiasMetricsEnabled(); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); - if (performanceMetricsEnabled) { - enabledTabs.push(MetricsTabKeys.PERFORMANCE); - } - if (biasMetricsEnabled) { - enabledTabs.push(MetricsTabKeys.BIAS); - } - return enabledTabs; -}; - -export default useMetricsPageEnabledTabs; diff --git a/frontend/src/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled.ts b/frontend/src/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled.ts deleted file mode 100644 index 7753bb0e11..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useAppContext } from '~/app/AppContext'; -import { featureFlagEnabled } from '~/utilities/utils'; - -const usePerformanceMetricsEnabled = () => { - const { - dashboardConfig: { - spec: { - dashboardConfig: { disablePerformanceMetrics }, - }, - }, - } = useAppContext(); - - return [featureFlagEnabled(disablePerformanceMetrics)]; -}; - -export default usePerformanceMetricsEnabled; diff --git a/frontend/src/pages/modelServing/screens/metrics/utils.ts b/frontend/src/pages/modelServing/screens/metrics/utils.ts new file mode 100644 index 0000000000..7868b3c1aa --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/utils.ts @@ -0,0 +1,87 @@ +import * as _ from 'lodash'; +import { SelectOptionObject } from '@patternfly/react-core'; +import { TimeframeTitle } from '~/pages/modelServing/screens/types'; +import { InferenceServiceKind } from '~/k8sTypes'; +import { DashboardConfig } from '~/types'; +import { ModelServingMetricType } from './ModelServingMetricsContext'; + +export const isModelMetricsEnabled = ( + dashboardNamespace: string, + dashboardConfig: DashboardConfig, +): boolean => { + if (dashboardNamespace === 'redhat-ods-applications') { + return true; + } + return dashboardConfig.spec.dashboardConfig.modelMetricsNamespace !== ''; +}; + +export const getInferenceServiceMetricsQueries = ( + inferenceService: InferenceServiceKind, +): Record => { + const namespace = inferenceService.metadata.namespace; + const name = inferenceService.metadata.name; + return { + [ModelServingMetricType.AVG_RESPONSE_TIME]: `query=sum(haproxy_backend_http_average_response_latency_milliseconds{exported_namespace="${namespace}", route="${name}"})`, + [ModelServingMetricType.ENDPOINT_HEALTH]: `query=sum(rate(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}", code=~"5xx"}[5m])) > 0`, + [ModelServingMetricType.FAILED_REQUEST_COUNT]: `query=sum(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}", code=~"4xx|5xx"})`, + [ModelServingMetricType.INFERENCE_PERFORMANCE]: `query=sum(rate(modelmesh_api_request_milliseconds_sum{namespace="${namespace}"}[5m]))`, + [ModelServingMetricType.REQUEST_COUNT]: `query=sum(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}"})`, + }; +}; + +export const isTimeframeTitle = ( + timeframe: string | SelectOptionObject, +): timeframe is TimeframeTitle => + Object.values(TimeframeTitle).includes(timeframe as TimeframeTitle); + +export const convertTimestamp = (timestamp: number, show?: 'date' | 'second'): string => { + const date = new Date(timestamp); + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + const month = months[date.getMonth()]; + const day = date.getDate(); + let hour = date.getHours(); + const minute = date.getMinutes(); + const second = date.getSeconds(); + const ampm = hour > 12 ? 'PM' : 'AM'; + hour = hour % 12; + hour = hour ? hour : 12; + const minuteString = minute < 10 ? '0' + minute : minute; + const secondString = second < 10 ? '0' + second : second; + return `${show === 'date' ? `${day} ${month} ` : ''}${hour}:${minuteString}${ + show === 'second' ? `:${secondString}` : '' + } ${ampm}`; +}; + +export const getThresholdData = ( + data: { x: number; y: number; name: string }[], + threshold: number, +): { x: number; y: number }[] => + _.uniqBy( + data.map((data) => ({ name: 'Threshold', x: data.x, y: threshold })), + (value) => value.x, + ); + +export const formatToShow = (timeframe: TimeframeTitle): 'date' | 'second' | undefined => { + switch (timeframe) { + case TimeframeTitle.FIVE_MINUTES: + return 'second'; + case TimeframeTitle.ONE_HOUR: + case TimeframeTitle.ONE_DAY: + return undefined; + default: + return 'date'; + } +}; diff --git a/frontend/src/pages/modelServing/screens/metrics/utils.tsx b/frontend/src/pages/modelServing/screens/metrics/utils.tsx deleted file mode 100644 index 60986805a8..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/utils.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import * as React from 'react'; -import * as _ from 'lodash'; -import { BreadcrumbItem, SelectOptionObject } from '@patternfly/react-core'; -import { Link } from 'react-router-dom'; -import { RefreshIntervalTitle, TimeframeTitle } from '~/pages/modelServing/screens/types'; -import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; -import { BreadcrumbItemType, DashboardConfig } from '~/types'; -import { - BiasSelectOption, - DomainCalculator, - GraphMetricLine, - GraphMetricPoint, - MetricChartLine, - MetricChartThreshold, - NamedMetricChartLine, - TranslatePoint, -} from '~/pages/modelServing/screens/metrics/types'; -import { BaseMetricRequest, BaseMetricRequestInput, BiasMetricType } from '~/api'; -import { BiasMetricConfig } from '~/concepts/explainability/types'; -import { - BIAS_CHART_CONFIGS, - BIAS_THRESHOLD_COLOR, -} from '~/pages/modelServing/screens/metrics/const'; -import { ModelMetricType, ServerMetricType } from './ModelServingMetricsContext'; - -export const isModelMetricsEnabled = ( - dashboardNamespace: string, - dashboardConfig: DashboardConfig, -): boolean => { - if (dashboardNamespace === 'redhat-ods-applications') { - return true; - } - return dashboardConfig.spec.dashboardConfig.modelMetricsNamespace !== ''; -}; - -export const getServerMetricsQueries = ( - server: ServingRuntimeKind, -): Record => { - const namespace = server.metadata.namespace; - return { - // TODO: Get new queries - [ServerMetricType.REQUEST_COUNT]: `TBD`, - [ServerMetricType.AVG_RESPONSE_TIME]: `rate(modelmesh_api_request_milliseconds_sum{exported_namespace="${namespace}"}[1m])/rate(modelmesh_api_request_milliseconds_count{exported_namespace="${namespace}"}[1m])`, - [ServerMetricType.CPU_UTILIZATION]: `TBD`, - [ServerMetricType.MEMORY_UTILIZATION]: `TBD`, - }; -}; - -export const getModelMetricsQueries = ( - model: InferenceServiceKind, -): Record => { - const namespace = model.metadata.namespace; - const name = model.metadata.name; - - return { - [ModelMetricType.REQUEST_COUNT_SUCCESS]: `sum(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}"})`, - [ModelMetricType.REQUEST_COUNT_FAILED]: `sum(haproxy_backend_http_responses_total{exported_namespace="${namespace}", route="${name}"})`, - [ModelMetricType.TRUSTY_AI_SPD]: `trustyai_spd{model="${name}"}`, - [ModelMetricType.TRUSTY_AI_DIR]: `trustyai_dir{model="${name}"}`, - }; -}; - -export const isTimeframeTitle = ( - timeframe: string | SelectOptionObject, -): timeframe is TimeframeTitle => - Object.values(TimeframeTitle).includes(timeframe as TimeframeTitle); - -export const isRefreshIntervalTitle = ( - refreshInterval: string | SelectOptionObject, -): refreshInterval is RefreshIntervalTitle => - Object.values(RefreshIntervalTitle).includes(refreshInterval as RefreshIntervalTitle); - -export const convertTimestamp = (timestamp: number, show?: 'date' | 'second'): string => { - const date = new Date(timestamp); - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; - const month = months[date.getMonth()]; - const day = date.getDate(); - let hour = date.getHours(); - const minute = date.getMinutes(); - const second = date.getSeconds(); - const ampm = hour > 12 ? 'PM' : 'AM'; - hour = hour % 12; - hour = hour ? hour : 12; - const minuteString = minute < 10 ? '0' + minute : minute; - const secondString = second < 10 ? '0' + second : second; - return `${show === 'date' ? `${day} ${month} ` : ''}${hour}:${minuteString}${ - show === 'second' ? `:${secondString}` : '' - } ${ampm}`; -}; - -export const getThresholdData = (data: GraphMetricLine[], threshold: number): GraphMetricLine => - _.uniqBy( - _.uniq( - data.reduce((xValues, line) => [...xValues, ...line.map((point) => point.x)], []), - ).map((xValue) => ({ - name: 'Threshold', - x: xValue, - y: threshold, - })), - (value) => value.x, - ); - -export const formatToShow = (timeframe: TimeframeTitle): 'date' | 'second' | undefined => { - switch (timeframe) { - case TimeframeTitle.ONE_HOUR: - case TimeframeTitle.ONE_DAY: - return undefined; - default: - return 'date'; - } -}; - -export const per100: TranslatePoint = (point) => ({ - ...point, - y: point.y / 100, -}); - -export const createGraphMetricLine = ({ - metric, - name, - translatePoint, -}: NamedMetricChartLine): GraphMetricLine => - metric.data?.map((data) => { - const point: GraphMetricPoint = { - x: data[0] * 1000, - y: parseFloat(data[1]), - name, - }; - if (translatePoint) { - return translatePoint(point); - } - return point; - }) || []; - -export const useStableMetrics = ( - metricChartLine: MetricChartLine, - chartTitle: string, -): NamedMetricChartLine[] => { - const metricsRef = React.useRef([]); - - const metrics = Array.isArray(metricChartLine) - ? metricChartLine - : [{ ...metricChartLine, name: metricChartLine.name ?? chartTitle }]; - - if ( - metrics.length !== metricsRef.current.length || - metrics.some((graphLine, i) => graphLine.metric !== metricsRef.current[i].metric) - ) { - metricsRef.current = metrics; - } - return metricsRef.current; -}; - -export const defaultDomainCalculator: DomainCalculator = (maxYValue) => ({ - y: maxYValue === 0 ? [0, 1] : [0, maxYValue], -}); - -export const getBreadcrumbItemComponents = (breadcrumbItems: BreadcrumbItemType[]) => - breadcrumbItems.map((item) => ( - (item.link ? {item.label} : <>{item.label})} - /> - )); - -const checkThresholdValid = (metricType: BiasMetricType, thresholdDelta?: number): boolean => { - if (thresholdDelta !== undefined) { - if (metricType === BiasMetricType.SPD) { - // SPD, no limitation, valid - return true; - } - - if (metricType === BiasMetricType.DIR) { - if (thresholdDelta >= 0 && thresholdDelta < 1) { - // 0<=DIR<1 , valid - return true; - } - // DIR, not within the range, invalid - return false; - } - // not SPD not DIR, undefined for now, metricType should be selected, invalid - return false; - } - // not input anything, invalid - return false; -}; - -const checkBatchSizeValid = (batchSize?: number): boolean => { - if (batchSize !== undefined) { - if (Number.isInteger(batchSize)) { - // size > 2, integer, valid - if (batchSize >= 2) { - return true; - } - // size <= 2, invalid - return false; - } - // not an integer, invalid - return false; - } - // not input anything, invalid - return false; -}; - -export const checkConfigurationFieldsValid = ( - configurations: BaseMetricRequest, - metricType?: BiasMetricType, -) => - metricType !== undefined && - configurations.requestName !== '' && - configurations.protectedAttribute !== '' && - configurations.privilegedAttribute !== '' && - configurations.unprivilegedAttribute !== '' && - configurations.outcomeName !== '' && - configurations.favorableOutcome !== '' && - configurations.batchSize !== undefined && - configurations.batchSize > 0 && - checkThresholdValid(metricType, configurations.thresholdDelta) && - checkBatchSizeValid(configurations.batchSize); - -export const isMetricType = ( - metricType: string | SelectOptionObject, -): metricType is BiasMetricType => - Object.values(BiasMetricType).includes(metricType as BiasMetricType); - -export const byId = - (arg: U) => - (arg2: T) => { - if (typeof arg === 'object') { - return arg2.id === arg.id; - } - return arg2.id === arg; - }; - -export const byNotId = - (arg: U) => - (arg2: T) => { - if (typeof arg === 'object') { - return arg2.id !== arg.id; - } - return arg2.id !== arg; - }; - -export const calculateThresholds = (origin: number, delta: number) => [ - origin + delta, - origin - delta, -]; - -export const createChartThresholds = (x: BiasMetricConfig): MetricChartThreshold[] => { - const { thresholdOrigin, defaultDelta } = BIAS_CHART_CONFIGS[x.metricType]; - const [maxThreshold, minThreshold] = calculateThresholds( - thresholdOrigin, - x.thresholdDelta ?? defaultDelta, - ); - - return [ - { - value: maxThreshold, - color: BIAS_THRESHOLD_COLOR, - }, - { - value: minThreshold, - color: BIAS_THRESHOLD_COLOR, - }, - ]; -}; -export const createBiasSelectOption = (biasMetricConfig: BiasMetricConfig): BiasSelectOption => { - const { id, name } = biasMetricConfig; - return { - id, - name, - biasMetricConfig, - toString: () => name, - compareTo: byId(id), - }; -}; -export const isBiasSelectOption = (obj: SelectOptionObject): obj is BiasSelectOption => - 'biasMetricConfig' in obj; - -export const convertInputType = (input: string) => { - if (input !== '' && !isNaN(Number(input))) { - return Number(input); - } - if (input.toLowerCase() === 'true') { - return true; - } - if (input.toLowerCase() === 'false') { - return false; - } - return input; -}; - -export const convertConfigurationRequestType = ( - configuration: BaseMetricRequestInput, -): BaseMetricRequest => ({ - ...configuration, - privilegedAttribute: convertInputType(configuration.privilegedAttribute), - unprivilegedAttribute: convertInputType(configuration.unprivilegedAttribute), - favorableOutcome: convertInputType(configuration.favorableOutcome), -}); - -export const getThresholdDefaultDelta = (metricType?: BiasMetricType) => - metricType && BIAS_CHART_CONFIGS[metricType].defaultDelta; diff --git a/frontend/src/pages/modelServing/screens/projects/DetailsPageMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/DetailsPageMetricsWrapper.tsx new file mode 100644 index 0000000000..74b4361916 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/projects/DetailsPageMetricsWrapper.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; +import MetricsPage from '~/pages/modelServing/screens/metrics/MetricsPage'; +import { ModelServingMetricsProvider } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import { getInferenceServiceMetricsQueries } from '~/pages/modelServing/screens/metrics/utils'; +import NotFound from '~/pages/NotFound'; +import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; +import { getProjectDisplayName } from '~/pages/projects/utils'; + +const DetailsPageMetricsWrapper: React.FC = () => { + const { namespace: projectName, inferenceService: modelName } = useParams<{ + namespace: string; + inferenceService: string; + }>(); + const { + currentProject, + inferenceServices: { data: models, loaded }, + } = React.useContext(ProjectDetailsContext); + const inferenceService = models.find( + (model) => model.metadata.name === modelName && model.metadata.namespace === projectName, + ); + if (!loaded) { + return ( + + + + ); + } + if (!inferenceService) { + return ; + } + const queries = getInferenceServiceMetricsQueries(inferenceService); + const projectDisplayName = getProjectDisplayName(currentProject); + const modelDisplayName = getInferenceServiceDisplayName(inferenceService); + + return ( + + + + ); +}; + +export default DetailsPageMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectInferenceExplainabilityWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectInferenceExplainabilityWrapper.tsx deleted file mode 100644 index e535bdb5a6..0000000000 --- a/frontend/src/pages/modelServing/screens/projects/ProjectInferenceExplainabilityWrapper.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Outlet, useParams } from 'react-router-dom'; -import React from 'react'; -import NotFound from '~/pages/NotFound'; -import { ExplainabilityContextProvider } from '~/concepts/explainability/ExplainabilityContext'; - -const ProjectInferenceExplainabilityWrapper: React.FC = () => { - const { namespace } = useParams<{ namespace: string }>(); - - if (!namespace) { - return ; - } - - return ( - - - - ); -}; - -export default ProjectInferenceExplainabilityWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsConfigurationPage.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsConfigurationPage.tsx deleted file mode 100644 index 8363a0d586..0000000000 --- a/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsConfigurationPage.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from 'react'; -import { useOutletContext } from 'react-router'; -import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; -import BiasConfigurationPage from '~/pages/modelServing/screens/metrics/BiasConfigurationPage'; -import { getProjectDisplayName } from '~/pages/projects/utils'; -import { ProjectModelMetricsOutletContextProps } from './ProjectModelMetricsWrapper'; - -const ProjectModelMetricsConfigurationPage: React.FC = () => { - const { currentProject, model } = useOutletContext(); - return ( - - ); -}; - -export default ProjectModelMetricsConfigurationPage; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsPage.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsPage.tsx deleted file mode 100644 index d7b18fd8f2..0000000000 --- a/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsPage.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import { useOutletContext } from 'react-router-dom'; -import { getInferenceServiceDisplayName } from '~/pages/modelServing/screens/global/utils'; -import MetricsPage from '~/pages/modelServing/screens/metrics/MetricsPage'; -import { getProjectDisplayName } from '~/pages/projects/utils'; -import { PerformanceMetricType } from '~/pages/modelServing/screens/types'; -import { ProjectModelMetricsOutletContextProps } from './ProjectModelMetricsWrapper'; - -const ProjectModelMetricsPage: React.FC = () => { - const { model, currentProject } = useOutletContext(); - const projectDisplayName = getProjectDisplayName(currentProject); - const modelDisplayName = getInferenceServiceDisplayName(model); - - return ( - - ); -}; - -export default ProjectModelMetricsPage; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsPathWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsPathWrapper.tsx deleted file mode 100644 index 69f533cf4d..0000000000 --- a/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsPathWrapper.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { useParams } from 'react-router-dom'; -import { Bullseye, Spinner } from '@patternfly/react-core'; -import NotFound from '~/pages/NotFound'; -import { InferenceServiceKind, ProjectKind } from '~/k8sTypes'; -import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; - -type ProjectModelMetricsPathWrapperProps = { - children: ( - inferenceService: InferenceServiceKind, - currentProject: ProjectKind, - ) => React.ReactNode; -}; - -const ProjectModelMetricsPathWrapper: React.FC = ({ - children, -}) => { - const { inferenceService: modelName } = useParams<{ - inferenceService: string; - }>(); - const { - currentProject, - inferenceServices: { data: models, loaded }, - } = React.useContext(ProjectDetailsContext); - const model = models.find((model) => model.metadata.name === modelName); - if (!loaded) { - return ( - - - - ); - } - if (!model) { - return ; - } - - return <>{children(model, currentProject)}; -}; - -export default ProjectModelMetricsPathWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsWrapper.tsx deleted file mode 100644 index 05900f7992..0000000000 --- a/frontend/src/pages/modelServing/screens/projects/ProjectModelMetricsWrapper.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { Outlet } from 'react-router-dom'; -import { ModelServingMetricsProvider } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { getModelMetricsQueries } from '~/pages/modelServing/screens/metrics/utils'; -import { PerformanceMetricType } from '~/pages/modelServing/screens/types'; -import { InferenceServiceKind, ProjectKind } from '~/k8sTypes'; -import ProjectModelMetricsPathWrapper from './ProjectModelMetricsPathWrapper'; - -export type ProjectModelMetricsOutletContextProps = { - model: InferenceServiceKind; - currentProject: ProjectKind; -}; - -const ProjectModelMetricsWrapper: React.FC = () => ( - - {(model, currentProject) => { - const queries = getModelMetricsQueries(model); - return ( - - - - ); - }} - -); - -export default ProjectModelMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectServerMetricsPathWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectServerMetricsPathWrapper.tsx deleted file mode 100644 index 9ac240e3c3..0000000000 --- a/frontend/src/pages/modelServing/screens/projects/ProjectServerMetricsPathWrapper.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from 'react'; -import { useParams } from 'react-router-dom'; -import { Bullseye, Spinner } from '@patternfly/react-core'; -import NotFound from '~/pages/NotFound'; -import { ProjectKind, ServingRuntimeKind } from '~/k8sTypes'; -import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; - -type ProjectServerMetricsPathWrapperProps = { - children: (servingRuntime: ServingRuntimeKind, currentProject: ProjectKind) => React.ReactNode; -}; - -const ProjectServerMetricsPathWrapper: React.FC = ({ - children, -}) => { - const { servingRuntime: serverName } = useParams<{ - servingRuntime: string; - }>(); - const { - currentProject, - servingRuntimes: { data: servers, loaded }, - } = React.useContext(ProjectDetailsContext); - const servingRuntime = servers.find((server) => server.metadata.name === serverName); - if (!loaded) { - return ( - - - - ); - } - if (!servingRuntime) { - return ; - } - - return <>{children(servingRuntime, currentProject)}; -}; - -export default ProjectServerMetricsPathWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ProjectServerMetricsWrapper.tsx b/frontend/src/pages/modelServing/screens/projects/ProjectServerMetricsWrapper.tsx deleted file mode 100644 index 15f82883ff..0000000000 --- a/frontend/src/pages/modelServing/screens/projects/ProjectServerMetricsWrapper.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react'; -import MetricsPage from '~/pages/modelServing/screens/metrics/MetricsPage'; -import { ModelServingMetricsProvider } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { getServerMetricsQueries } from '~/pages/modelServing/screens/metrics/utils'; -import { getProjectDisplayName } from '~/pages/projects/utils'; -import { PerformanceMetricType } from '~/pages/modelServing/screens/types'; -import ProjectServerMetricsPathWrapper from './ProjectServerMetricsPathWrapper'; -import { getModelServerDisplayName } from './utils'; - -const ProjectServerMetricsWrapper: React.FC = () => ( - - {(servingRuntime, currentProject) => { - const queries = getServerMetricsQueries(servingRuntime); - const projectDisplayName = getProjectDisplayName(currentProject); - const serverName = getModelServerDisplayName(servingRuntime); - return ( - - - - ); - }} - -); - -export default ProjectServerMetricsWrapper; diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx index c5d0df119a..238711781c 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx @@ -1,15 +1,13 @@ import * as React from 'react'; -import { Button, DropdownDirection, Icon, Skeleton, Tooltip } from '@patternfly/react-core'; +import { Button, Icon, Skeleton, Tooltip } from '@patternfly/react-core'; import { ActionsColumn, Tbody, Td, Tr } from '@patternfly/react-table'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; -import { useNavigate } from 'react-router-dom'; import { ServingRuntimeKind } from '~/k8sTypes'; import EmptyTableCellForAlignment from '~/pages/projects/components/EmptyTableCellForAlignment'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; import { ServingRuntimeTableTabs } from '~/pages/modelServing/screens/types'; import { ProjectSectionID } from '~/pages/projects/screens/detail/types'; import { getDisplayNameFromServingRuntimeTemplate } from '~/pages/modelServing/customServingRuntimes/utils'; -import usePerformanceMetricsEnabled from '~/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled'; import ServingRuntimeTableExpandedSection from './ServingRuntimeTableExpandedSection'; import { getInferenceServiceFromServingRuntime, isServingRuntimeTokenEnabled } from './utils'; @@ -26,12 +24,9 @@ const ServingRuntimeTableRow: React.FC = ({ onEditServingRuntime, onDeployModal, }) => { - const navigate = useNavigate(); - const [expandedColumn, setExpandedColumn] = React.useState(); const { - currentProject, inferenceServices: { data: inferenceServices, loaded: inferenceServicesLoaded, @@ -45,8 +40,6 @@ const ServingRuntimeTableRow: React.FC = ({ const modelInferenceServices = getInferenceServiceFromServingRuntime(inferenceServices, obj); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); - const onToggle = (_, __, colIndex: ServingRuntimeTableTabs) => { setExpandedColumn(expandedColumn === colIndex ? undefined : colIndex); }; @@ -134,7 +127,6 @@ const ServingRuntimeTableRow: React.FC = ({
diff --git a/frontend/src/pages/modelServing/screens/projects/utils.ts b/frontend/src/pages/modelServing/screens/projects/utils.ts index 0e97f72cee..616789f8c1 100644 --- a/frontend/src/pages/modelServing/screens/projects/utils.ts +++ b/frontend/src/pages/modelServing/screens/projects/utils.ts @@ -206,6 +206,3 @@ export const useCreateInferenceServiceObject = ( return createInferenceServiceState; }; - -export const getModelServerDisplayName = (server: ServingRuntimeKind) => - getDisplayNameFromK8sResource(server); diff --git a/frontend/src/pages/modelServing/screens/types.ts b/frontend/src/pages/modelServing/screens/types.ts index c6c0e31411..7b66c456c5 100644 --- a/frontend/src/pages/modelServing/screens/types.ts +++ b/frontend/src/pages/modelServing/screens/types.ts @@ -1,17 +1,11 @@ import { EnvVariableDataEntry } from '~/pages/projects/types'; import { ContainerResources } from '~/types'; -export enum PerformanceMetricType { - SERVER = 'server', - MODEL = 'model', -} - export enum TimeframeTitle { + FIVE_MINUTES = '5 minutes', ONE_HOUR = '1 hour', ONE_DAY = '24 hours', - ONE_WEEK = '7 days', - ONE_MONTH = '30 days', - // UNLIMITED = 'Unlimited', + ONE_WEEK = '1 week', } export type TimeframeTimeType = { @@ -20,22 +14,6 @@ export type TimeframeTimeType = { export type TimeframeStepType = TimeframeTimeType; -export enum RefreshIntervalTitle { - FIFTEEN_SECONDS = '15 seconds', - THIRTY_SECONDS = '30 seconds', - ONE_MINUTE = '1 minute', - FIVE_MINUTES = '5 minutes', - FIFTEEN_MINUTES = '15 minutes', - THIRTY_MINUTES = '30 minutes', - ONE_HOUR = '1 hour', - TWO_HOURS = '2 hours', - ONE_DAY = '1 day', -} - -export type RefreshIntervalValueType = { - [key in RefreshIntervalTitle]: number; -}; - export enum ServingRuntimeTableTabs { TYPE = 1, DEPLOYED_MODELS = 2, diff --git a/frontend/src/pages/modelServing/useModelMetricsEnabled.ts b/frontend/src/pages/modelServing/useModelMetricsEnabled.ts index a156a7a96e..94d31a8878 100644 --- a/frontend/src/pages/modelServing/useModelMetricsEnabled.ts +++ b/frontend/src/pages/modelServing/useModelMetricsEnabled.ts @@ -1,20 +1,16 @@ -import { useAppContext } from '~/app/AppContext'; -import { useDashboardNamespace } from '~/redux/selectors'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; -import { isModelMetricsEnabled } from './screens/metrics/utils'; -import usePerformanceMetricsEnabled from './screens/metrics/usePerformanceMetricsEnabled'; +// import { useAppContext } from '~/app/AppContext'; +// import { useDashboardNamespace } from '~/redux/selectors'; +// import { isModelMetricsEnabled } from './screens/metrics/utils'; +// +// const useModelMetricsEnabled = (): [modelMetricsEnabled: boolean] => { +// const { dashboardNamespace } = useDashboardNamespace(); +// const { dashboardConfig } = useAppContext(); +// +// const checkModelMetricsEnabled = () => isModelMetricsEnabled(dashboardNamespace, dashboardConfig); +// +// return [checkModelMetricsEnabled()]; +// }; -const useModelMetricsEnabled = (): [modelMetricsEnabled: boolean] => { - const { dashboardNamespace } = useDashboardNamespace(); - const { dashboardConfig } = useAppContext(); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); - const [biasMetricsEnabled] = useBiasMetricsEnabled(); - - const checkModelMetricsEnabled = () => - isModelMetricsEnabled(dashboardNamespace, dashboardConfig) && - (performanceMetricsEnabled || biasMetricsEnabled); - - return [checkModelMetricsEnabled()]; -}; +const useModelMetricsEnabled = () => [false]; export default useModelMetricsEnabled; diff --git a/frontend/src/pages/pipelines/global/pipelines/PipelinesView.tsx b/frontend/src/pages/pipelines/global/pipelines/PipelinesView.tsx index 52680ed425..0dd34f0bff 100644 --- a/frontend/src/pages/pipelines/global/pipelines/PipelinesView.tsx +++ b/frontend/src/pages/pipelines/global/pipelines/PipelinesView.tsx @@ -4,7 +4,7 @@ import GlobalNoPipelines from '~/pages/pipelines/global/pipelines/GlobalNoPipeli import PipelinesTable from '~/concepts/pipelines/content/tables/pipeline/PipelinesTable'; import usePipelines from '~/concepts/pipelines/apiHooks/usePipelines'; import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage'; -import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; +import EmptyTableView from '~/concepts/pipelines/content/tables/EmptyTableView'; import GlobalPipelinesTableToolbar, { FilterType, FilterData } from './GlobalPipelinesTableToolbar'; const DEFAULT_FILTER_DATA: FilterData = { @@ -61,9 +61,7 @@ const PipelinesView: React.FC = () => { onClearFilters={() => setFilterData(DEFAULT_FILTER_DATA)} /> } - emptyTableView={ - setFilterData(DEFAULT_FILTER_DATA)} /> - } + emptyTableView={ setFilterData(DEFAULT_FILTER_DATA)} />} refreshPipelines={refresh} pipelineDetailsPath={(namespace, id) => `/pipelines/${namespace}/pipeline/view/${id}`} /> diff --git a/frontend/src/pages/projects/ProjectDetailsContext.tsx b/frontend/src/pages/projects/ProjectDetailsContext.tsx index d058a90844..2a0f3162d9 100644 --- a/frontend/src/pages/projects/ProjectDetailsContext.tsx +++ b/frontend/src/pages/projects/ProjectDetailsContext.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import { Navigate, Outlet, useParams } from 'react-router-dom'; import { - GroupKind, - InferenceServiceKind, + ServingRuntimeKind, PersistentVolumeClaimKind, ProjectKind, - RoleBindingKind, + InferenceServiceKind, SecretKind, - ServingRuntimeKind, + RoleBindingKind, + GroupKind, TemplateKind, } from '~/k8sTypes'; import { DEFAULT_CONTEXT_DATA } from '~/utilities/const'; diff --git a/frontend/src/pages/projects/ProjectViewRoutes.tsx b/frontend/src/pages/projects/ProjectViewRoutes.tsx index 90b54728be..83f6d81148 100644 --- a/frontend/src/pages/projects/ProjectViewRoutes.tsx +++ b/frontend/src/pages/projects/ProjectViewRoutes.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { Navigate, Route } from 'react-router-dom'; -import ProjectModelMetricsWrapper from '~/pages/modelServing/screens/projects/ProjectModelMetricsWrapper'; -import ProjectServerMetricsWrapper from '~/pages/modelServing/screens/projects/ProjectServerMetricsWrapper'; +import DetailsPageMetricsWrapper from '~/pages/modelServing/screens/projects/DetailsPageMetricsWrapper'; import useModelMetricsEnabled from '~/pages/modelServing/useModelMetricsEnabled'; import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes'; import ProjectPipelineBreadcrumbPage from '~/pages/projects/screens/detail/pipelines/ProjectPipelineBreadcrumbPage'; @@ -9,11 +8,6 @@ import PipelineDetails from '~/concepts/pipelines/content/pipelinesDetails/pipel import PipelineRunDetails from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails'; import CreateRunPage from '~/concepts/pipelines/content/createRun/CreateRunPage'; import CloneRunPage from '~/concepts/pipelines/content/createRun/CloneRunPage'; -import ProjectModelMetricsConfigurationPage from '~/pages/modelServing/screens/projects/ProjectModelMetricsConfigurationPage'; -import ProjectModelMetricsPage from '~/pages/modelServing/screens/projects/ProjectModelMetricsPage'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; -import usePerformanceMetricsEnabled from '~/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled'; -import ProjectInferenceExplainabilityWrapper from '~/pages/modelServing/screens/projects/ProjectInferenceExplainabilityWrapper'; import ProjectDetails from './screens/detail/ProjectDetails'; import ProjectView from './screens/projects/ProjectView'; import ProjectDetailsContextProvider from './ProjectDetailsContext'; @@ -22,8 +16,6 @@ import EditSpawnerPage from './screens/spawner/EditSpawnerPage'; const ProjectViewRoutes: React.FC = () => { const [modelMetricsEnabled] = useModelMetricsEnabled(); - const [biasMetricsEnabled] = useBiasMetricsEnabled(); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); return ( @@ -32,26 +24,13 @@ const ProjectViewRoutes: React.FC = () => { } /> } /> } /> - {modelMetricsEnabled && ( - <> - }> - } /> - }> - } /> - {biasMetricsEnabled && ( - } /> - )} - - } /> - - {performanceMetricsEnabled && ( - } - /> - )} - - )} + : + } + /> + } diff --git a/frontend/src/concepts/dashboard/DashboardSearchField.tsx b/frontend/src/pages/projects/components/SearchField.tsx similarity index 77% rename from frontend/src/concepts/dashboard/DashboardSearchField.tsx rename to frontend/src/pages/projects/components/SearchField.tsx index c7e6713c44..7aae49d555 100644 --- a/frontend/src/concepts/dashboard/DashboardSearchField.tsx +++ b/frontend/src/pages/projects/components/SearchField.tsx @@ -1,20 +1,13 @@ import * as React from 'react'; import { InputGroup, SearchInput, Select, SelectOption } from '@patternfly/react-core'; -// List all the possible search fields here export enum SearchType { NAME = 'Name', USER = 'User', PROJECT = 'Project', - METRIC = 'Metric', - PROTECTED_ATTRIBUTE = 'Protected attribute', - PRIVILEGED_VALUE = 'Privileged value', - UNPRIVILEGED_VALUE = 'Unprivileged value', - OUTPUT = 'Output', - OUTPUT_VALUE = 'Output value', } -type DashboardSearchFieldProps = { +type SearchFieldProps = { types: string[]; searchType: SearchType; onSearchTypeChange: (searchType: SearchType) => void; @@ -22,7 +15,7 @@ type DashboardSearchFieldProps = { onSearchValueChange: (searchValue: string) => void; }; -const DashboardSearchField: React.FC = ({ +const SearchField: React.FC = ({ types, searchValue, searchType, @@ -64,4 +57,4 @@ const DashboardSearchField: React.FC = ({ ); }; -export default DashboardSearchField; +export default SearchField; diff --git a/frontend/src/pages/projects/projectSettings/ModelBiasSettingsCard.tsx b/frontend/src/pages/projects/projectSettings/ModelBiasSettingsCard.tsx deleted file mode 100644 index 3c5fb4fd81..0000000000 --- a/frontend/src/pages/projects/projectSettings/ModelBiasSettingsCard.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { - Alert, - AlertActionCloseButton, - Card, - CardBody, - CardFooter, - CardHeader, - CardTitle, - Stack, - StackItem, -} from '@patternfly/react-core'; -import React from 'react'; -import InstallTrustyAICheckbox, { - TrustyAICRActions, -} from '~/concepts/explainability/content/InstallTrustyAICheckbox'; - -type ModelBiasSettingsCardProps = { - namespace: string; -}; -const ModelBiasSettingsCard: React.FC = ({ namespace }) => { - const [notifyAction, setNotifyAction] = React.useState(undefined); - const [success, setSuccess] = React.useState(false); - const [error, setError] = React.useState(undefined); - - const clearNotification = React.useCallback(() => { - setNotifyAction(undefined); - setSuccess(false); - setError(undefined); - }, []); - - const renderNotification = () => { - if (success && notifyAction === TrustyAICRActions.CREATE) { - return ( - } - isLiveRegion - isInline - > - The TrustAI service was successfully installed - - ); - } - - if (!success && notifyAction === TrustyAICRActions.CREATE) { - return ( - } - isLiveRegion - isInline - > - {error?.message} - - ); - } - - if (success && notifyAction === TrustyAICRActions.DELETE) { - return ( - } - isLiveRegion - isInline - > - The TrustAI service was successfully uninstalled - - ); - } - - if (!success && notifyAction === TrustyAICRActions.DELETE) { - return ( - } - isLiveRegion - isInline - > - {error?.message} - - ); - } - - return null; - }; - - return ( - - - Model Bias - - - - - { - setNotifyAction(action); - setSuccess(success); - setError(error); - }} - /> - - - - {renderNotification()} - - ); -}; - -export default ModelBiasSettingsCard; diff --git a/frontend/src/pages/projects/projectSettings/ProjectSettingsPage.tsx b/frontend/src/pages/projects/projectSettings/ProjectSettingsPage.tsx deleted file mode 100644 index c915118ded..0000000000 --- a/frontend/src/pages/projects/projectSettings/ProjectSettingsPage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { PageSection, Stack, StackItem } from '@patternfly/react-core'; -import ModelBiasSettingsCard from '~/pages/projects/projectSettings/ModelBiasSettingsCard'; -import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; - -const ProjectSettingsPage = () => { - const { currentProject } = React.useContext(ProjectDetailsContext); - const namespace = currentProject.metadata.name; - const [biasMetricsEnabled] = useBiasMetricsEnabled(); - - return ( - - - {biasMetricsEnabled && ( - - - - )} - - - ); -}; - -export default ProjectSettingsPage; diff --git a/frontend/src/pages/projects/projectSettings/const.ts b/frontend/src/pages/projects/projectSettings/const.ts deleted file mode 100644 index fb2034bd3b..0000000000 --- a/frontend/src/pages/projects/projectSettings/const.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const TRUSTYAI_TOOLTIP_TEXT = - 'Selecting this will install the TrustyAI service in your namespace. TrustyAI receives data from ModelMesh that is used to calculate and display various measures of model bias over time.'; diff --git a/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx b/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx index 8ce00c920a..1456054b09 100644 --- a/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx +++ b/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; -import { CogIcon, CubeIcon, UsersIcon } from '@patternfly/react-icons'; +import { CubeIcon, UsersIcon } from '@patternfly/react-icons'; import { Link } from 'react-router-dom'; import ApplicationsPage from '~/pages/ApplicationsPage'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; @@ -11,7 +11,6 @@ import { useAppContext } from '~/app/AppContext'; import { useAccessReview } from '~/api'; import { isProjectSharingEnabled } from '~/pages/projects/projectSharing/utils'; import { AccessReviewResourceAttributes } from '~/k8sTypes'; -import ProjectSettingsPage from '~/pages/projects/projectSettings/ProjectSettingsPage'; import useCheckLogoutParams from './useCheckLogoutParams'; import ProjectDetailsComponents from './ProjectDetailsComponents'; @@ -53,7 +52,6 @@ const ProjectDetails: React.FC = () => { sections={[ { title: 'Components', component: , icon: }, { title: 'Permissions', component: , icon: }, - { title: 'Settings', component: , icon: }, ]} /> ) : ( diff --git a/frontend/src/pages/projects/screens/projects/ProjectListView.tsx b/frontend/src/pages/projects/screens/projects/ProjectListView.tsx index 9c00034481..668536a446 100644 --- a/frontend/src/pages/projects/screens/projects/ProjectListView.tsx +++ b/frontend/src/pages/projects/screens/projects/ProjectListView.tsx @@ -1,14 +1,13 @@ import * as React from 'react'; -import { ButtonVariant, ToolbarItem } from '@patternfly/react-core'; +import { Button, ButtonVariant, ToolbarItem } from '@patternfly/react-core'; import { useNavigate } from 'react-router-dom'; import Table from '~/components/table/Table'; import useTableColumnSort from '~/components/table/useTableColumnSort'; +import SearchField, { SearchType } from '~/pages/projects/components/SearchField'; import { ProjectKind } from '~/k8sTypes'; import { getProjectDisplayName, getProjectOwner } from '~/pages/projects/utils'; import LaunchJupyterButton from '~/pages/projects/screens/projects/LaunchJupyterButton'; import { ProjectsContext } from '~/concepts/projects/ProjectsContext'; -import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField'; -import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; import NewProjectButton from './NewProjectButton'; import { columns } from './tableData'; import ProjectTableRow from './ProjectTableRow'; @@ -44,12 +43,7 @@ const ProjectListView: React.FC = ({ allowCreate }) => { setSearch(''); }; - const searchTypes = Object.keys(SearchType).filter( - (key) => - SearchType[key] === SearchType.NAME || - SearchType[key] === SearchType.PROJECT || - SearchType[key] === SearchType.USER, - ); + const searchTypes = React.useMemo(() => Object.keys(SearchType), []); const [deleteData, setDeleteData] = React.useState(); const [editData, setEditData] = React.useState(); @@ -61,7 +55,14 @@ const ProjectListView: React.FC = ({ allowCreate }) => { enablePagination data={filteredProjects} columns={columns} - emptyTableView={} + emptyTableView={ + <> + No projects match your filters.{' '} + + + } rowRenderer={(project) => ( = ({ allowCreate }) => { toolbarContent={ - void) => { - const cb = React.useRef(() => undefined); - - cb.current = callback; - - React.useEffect(() => { - const timer = setInterval(cb.current, refreshInterval); - return () => clearInterval(timer); - }, [refreshInterval]); -}; -export default useRefreshInterval; diff --git a/frontend/src/utilities/useRestructureContextResourceData.tsx b/frontend/src/utilities/useRestructureContextResourceData.tsx deleted file mode 100644 index cf4ae8618c..0000000000 --- a/frontend/src/utilities/useRestructureContextResourceData.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { FetchState } from '~/utilities/useFetchState'; -import { ContextResourceData } from '~/types'; - -const useRestructureContextResourceData = ( - resourceData: FetchState, -): ContextResourceData => { - const [data, loaded, error, refresh] = resourceData; - return React.useMemo( - () => ({ - data, - loaded, - error, - refresh, - }), - [data, error, loaded, refresh], - ); -}; - -export default useRestructureContextResourceData; diff --git a/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml b/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml index cb31acf28d..d75f306ee6 100644 --- a/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml +++ b/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml @@ -53,10 +53,6 @@ spec: type: string disablePipelines: type: boolean - disableBiasMetrics: - type: boolean - disablePerformanceMetrics: - type: boolean groupsConfig: type: object required: diff --git a/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml b/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml index b929959424..159ab96837 100644 --- a/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml +++ b/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml @@ -19,8 +19,6 @@ spec: disableProjectSharing: true disableCustomServingRuntimes: true modelMetricsNamespace: '' - disableBiasMetrics: true - disablePerformanceMetrics: true notebookController: enabled: true notebookSizes:
setExpanded(!isExpanded), - }} - /> - {obj.name}{obj.metricType}{obj.protectedAttribute}{obj.privilegedAttribute}{obj.unprivilegedAttribute}{obj.outcomeName}{obj.favorableOutcome} - { - onCloneConfiguration(obj); - }, - }, - { - title: 'Delete', - onClick: () => { - onDeleteConfiguration(obj); - }, - }, - ]} - /> -
- - - - - Violation threshold - {obj.thresholdDelta} - - - Metric batch size - {obj.batchSize} - - - -
= ({ title: 'Delete model server', onClick: () => onDeleteServingRuntime(obj), }, - ...(performanceMetricsEnabled - ? [ - { - title: 'View metrics', - onClick: () => - navigate( - `/projects/${currentProject.metadata.name}/metrics/server/${obj.metadata.name}`, - ), - }, - ] - : []), ]} />