diff --git a/docs/dev-setup.md b/docs/dev-setup.md index eea4d051f8..947debb2f1 100644 --- a/docs/dev-setup.md +++ b/docs/dev-setup.md @@ -7,7 +7,7 @@ ODH requires the following to run: - [NodeJS and NPM](https://nodejs.org/) - Node recommended version -> `18.16.0` - NPM recommended version -> `9.6.7` -- [OpenShift CLI](https://docs.openshift.com/container-platform/4.12/cli_reference/openshift_cli/getting-started-cli.html) +- [OpenShift CLI](https://docs.openshift.com/container-platform/latest/cli_reference/openshift_cli/getting-started-cli.html) - [kustomize](https://github.com/kubernetes-sigs/kustomize) (if you need to do deployment) ### Additional tooling diff --git a/frontend/.env b/frontend/.env index 6520afc0ce..111c155b89 100644 --- a/frontend/.env +++ b/frontend/.env @@ -8,4 +8,4 @@ ODH_FAVICON=rhoai-favicon.svg ODH_NOTEBOOK_REPO=opendatahub ########## Change this version with each dashboard release ########### -INTERNAL_DASHBOARD_VERSION=v2.23.0 +INTERNAL_DASHBOARD_VERSION=v2.24.0 diff --git a/frontend/.eslintrc b/frontend/.eslintrc index cca83da5be..ec95917b9b 100755 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -315,6 +315,18 @@ } ] } + }, + { + "files": ["*.ts", "*.tsx"], + "excludedFiles": ["**/__mocks__/**", "**/__tests__/**"], + "rules": { + "@typescript-eslint/consistent-type-assertions": [ + "error", + { + "assertionStyle": "never" + } + ] + } } ] } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7814bcc5d2..177a1f4edd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,7 +23,7 @@ "@patternfly/react-styles": "^5.2.1", "@patternfly/react-table": "^5.2.1", "@patternfly/react-tokens": "^5.2.1", - "@patternfly/react-topology": "^5.4.0-prerelease.6", + "@patternfly/react-topology": "^5.4.0-prerelease.9", "@patternfly/react-virtualized-extension": "^5.0.0", "@types/classnames": "^2.3.1", "axios": "^1.6.4", @@ -2239,6 +2239,22 @@ "ms": "^2.1.1" } }, + "node_modules/@dagrejs/dagre": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.2.tgz", + "integrity": "sha512-F09dphqvHsbe/6C2t2unbmpr5q41BNPEfJCdn8Z7aEBpVSy/zFQ/b4SWsweQjWNsYMDvE2ffNUN8X0CeFsEGNw==", + "dependencies": { + "@dagrejs/graphlib": "2.2.2" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.2.tgz", + "integrity": "sha512-CbyGpCDKsiTg/wuk79S7Muoj8mghDGAESWGxcSyhHX5jD35vYMBZochYVFzlHxynpE9unpu6O+4ZuhrLxASsOg==", + "engines": { + "node": ">17.0.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -4074,19 +4090,18 @@ "integrity": "sha512-VYK0uVP2/2RJ7ZshJCCLeq0Boih5I1bv+9Z/Bg6h12dCkLs85XsxAX9Ve+BGIo5DF54/mzcRHE1RKYap4ISXuw==" }, "node_modules/@patternfly/react-topology": { - "version": "5.4.0-prerelease.6", - "resolved": "https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-5.4.0-prerelease.6.tgz", - "integrity": "sha512-suyY+UOW9GgcDjt0HMHTKYx9UePdHAF3BSZN0Xt1zFtgR3AFMl3DOuAeYva4Yyk18OkA0D9gRe29283kgG536Q==", + "version": "5.4.0-prerelease.9", + "resolved": "https://registry.npmjs.org/@patternfly/react-topology/-/react-topology-5.4.0-prerelease.9.tgz", + "integrity": "sha512-mECBtZ+MsnOwONM56a5x0YGjHg+Z7D6AOIGEq2Tz1DRDTq2hnw+naMLq+oQRs8ijSqhpvaA/IYHY3Bi/CW412A==", "dependencies": { + "@dagrejs/dagre": "1.1.2", "@patternfly/react-core": "^5.1.1", "@patternfly/react-icons": "^5.1.1", "@patternfly/react-styles": "^5.1.1", "@types/d3": "^7.4.0", "@types/d3-force": "^1.2.1", - "@types/dagre": "0.7.42", "@types/react-measure": "^2.0.6", "d3": "^7.8.0", - "dagre": "0.8.2", "mobx": "^6.9.0", "mobx-react": "^7.6.0", "point-in-svg-path": "^1.0.1", @@ -4756,11 +4771,6 @@ "@types/d3-selection": "*" } }, - "node_modules/@types/dagre": { - "version": "0.7.42", - "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.42.tgz", - "integrity": "sha512-knVdi1Ul8xYgJ0wdhQ+/2YGJFKJFa/5srcPII9zvOs4KhsHfpnFrSTQXATYmjslglxRMif3Lg+wEZ0beag+94A==" - }, "node_modules/@types/dompurify": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", @@ -9284,15 +9294,6 @@ "node": ">=12" } }, - "node_modules/dagre": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.2.tgz", - "integrity": "sha512-TEOOGZOkCOgCG7AoUIq64sJ3d21SMv8tyoqteLpX+UsUsS9Qw8iap4hhogXY4oB3r0bbZuAjO0atAilgCmsE0Q==", - "dependencies": { - "graphlib": "^2.1.5", - "lodash": "^4.17.4" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -11836,14 +11837,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "devOptional": true }, - "node_modules/graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "dependencies": { - "lodash": "^4.17.15" - } - }, "node_modules/grpc-web": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/grpc-web/-/grpc-web-1.5.0.tgz", @@ -23400,9 +23393,9 @@ } }, "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "engines": { "node": ">=8.3.0" @@ -23968,9 +23961,9 @@ } }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "devOptional": true, "engines": { "node": ">=10.0.0" diff --git a/frontend/package.json b/frontend/package.json index c6ed706f3d..49d42f0ab1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -66,7 +66,7 @@ "@patternfly/react-styles": "^5.2.1", "@patternfly/react-table": "^5.2.1", "@patternfly/react-tokens": "^5.2.1", - "@patternfly/react-topology": "^5.4.0-prerelease.6", + "@patternfly/react-topology": "^5.4.0-prerelease.9", "@patternfly/react-virtualized-extension": "^5.0.0", "@types/classnames": "^2.3.1", "axios": "^1.6.4", diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunTable.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunTable.ts index 662c31ea89..31c8527789 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunTable.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunTable.ts @@ -10,15 +10,21 @@ class PipelineRunsRow extends TableRow { findColumnName(name: string) { return this.find().find(`[data-label=Name]`).contains(name); } +} - findColumnVersion(name: string) { - return this.find().find(`[data-label="Pipeline"]`).contains(name); +class PipelineRunTableRow extends PipelineRunsRow { + findKebabAction(name: string): Cypress.Chainable> { + this.find().findKebab().click(); + return cy.findByTestId('pipeline-run-table-row-actions').findByRole('menuitem', { name }); } +} +class PipelineRunJobTableRow extends PipelineRunsRow { findStatusSwitchByRowName() { return this.find().findByTestId('job-status-switch'); } } + class PipelineRunsTable { protected testId = ''; @@ -33,12 +39,6 @@ class PipelineRunsTable { return cy.findByTestId(this.testId); } - getRowByName(name: string) { - return new PipelineRunsRow(() => - this.find().find(`[data-label=Name]`).contains(name).parents('tr'), - ); - } - shouldRowNotBeVisible(name: string) { this.find() .parents() @@ -120,6 +120,12 @@ class ActiveRunsTable extends PipelineRunsTable { mockGetActiveRuns(runs: PipelineRunKFv2[], namespace: string, times?: number) { return this.mockGetRuns(runs, [], namespace, times); } + + getRowByName(name: string) { + return new PipelineRunTableRow(() => + this.find().find(`[data-label=Name]`).contains(name).parents('tr'), + ); + } } class ArchivedRunsTable extends PipelineRunsTable { constructor() { @@ -129,6 +135,12 @@ class ArchivedRunsTable extends PipelineRunsTable { mockGetArchivedRuns(runs: PipelineRunKFv2[], namespace: string, times?: number) { return this.mockGetRuns([], runs, namespace, times); } + + getRowByName(name: string) { + return new PipelineRunTableRow(() => + this.find().find(`[data-label=Name]`).contains(name).parents('tr'), + ); + } } class PipelineRunJobTable extends PipelineRunsTable { @@ -136,6 +148,12 @@ class PipelineRunJobTable extends PipelineRunsTable { super('schedules'); } + getRowByName(name: string) { + return new PipelineRunJobTableRow(() => + this.find().find(`[data-label=Name]`).contains(name).parents('tr'), + ); + } + findEmptyState() { return cy.findByTestId('schedules-empty-state'); } diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelineCreateRuns.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelineCreateRuns.cy.ts index 7d361cf97d..4e270a2e84 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelineCreateRuns.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelineCreateRuns.cy.ts @@ -267,7 +267,7 @@ describe('Pipeline create runs', () => { }, double_param: { parameterType: InputDefinitionParameterType.DOUBLE, - defaultValue: 0.1, + defaultValue: 7.0, description: 'Some double helper text', isOptional: true, }, @@ -313,7 +313,7 @@ describe('Pipeline create runs', () => { paramsSection.findParamById('string_param').should('have.value', 'String default value'); cy.findByTestId('string_param-helper-text').should('have.text', 'Some string helper text'); - paramsSection.findParamById('double_param').should('have.value', '0.1'); + paramsSection.findParamById('double_param').should('have.value', '7.0'); cy.findByTestId('double_param-form-group').should('not.have.text', '*', { exact: false }); cy.findByTestId('double_param-helper-text').should('have.text', 'Some double helper text'); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts index 0b72664277..1027f05a87 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts @@ -126,7 +126,7 @@ describe('Pipelines', () => { dspVersion: 'v2', objectStorage: { externalStorage: { - host: 's3.amazonaws.com/', + host: 's3.amazonaws.com', scheme: 'https', bucket: 'sdsd', region: 'us-east-1', diff --git a/frontend/src/__tests__/unit/testUtils/hooks.ts b/frontend/src/__tests__/unit/testUtils/hooks.ts index c82a2ff7cd..86c308422f 100644 --- a/frontend/src/__tests__/unit/testUtils/hooks.ts +++ b/frontend/src/__tests__/unit/testUtils/hooks.ts @@ -67,8 +67,8 @@ export const renderHook = < options?: RenderHookOptions, ): RenderHookResultExt => { let updateCount = 0; - let prevResult: Result | undefined; - let currentResult: Result | undefined; + let prevResult: Result; + let currentResult: Result; const renderResult = renderHookRTL((props) => { updateCount++; @@ -80,8 +80,7 @@ export const renderHook = < const renderResultExt: RenderHookResultExt = { ...renderResult, - getPreviousResult: () => - updateCount > 1 ? (prevResult as Result) : renderResult.result.current, + getPreviousResult: () => (updateCount > 1 ? prevResult : renderResult.result.current), getUpdateCount: () => updateCount, diff --git a/frontend/src/api/errorUtils.ts b/frontend/src/api/errorUtils.ts index e972b16c41..1ac6f1c5e9 100644 --- a/frontend/src/api/errorUtils.ts +++ b/frontend/src/api/errorUtils.ts @@ -2,7 +2,7 @@ import { K8sStatus } from '@openshift/dynamic-plugin-sdk-utils'; import { AxiosError } from 'axios'; export const isK8sStatus = (data: unknown): data is K8sStatus => - (data as K8sStatus).kind === 'Status'; + typeof data === 'object' && data !== null && 'kind' in data && data.kind === 'Status'; export class K8sStatusError extends Error { public statusObject: K8sStatus; @@ -18,6 +18,7 @@ const isAxiosErrorWithResponseMessage = ( error?: Error | AxiosError, ): error is AxiosError<{ message: string }> => Boolean( + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions error && typeof (error as AxiosError<{ message: string }>).response?.data.message === 'string', ); diff --git a/frontend/src/api/modelRegistry/errorUtils.ts b/frontend/src/api/modelRegistry/errorUtils.ts index d9a3238d74..1282802199 100644 --- a/frontend/src/api/modelRegistry/errorUtils.ts +++ b/frontend/src/api/modelRegistry/errorUtils.ts @@ -2,7 +2,7 @@ import { ModelRegistryError } from '~/concepts/modelRegistry/types'; import { isCommonStateError } from '~/utilities/useFetchState'; const isError = (e: unknown): e is ModelRegistryError => - ['code', 'message'].every((key) => key in (e as ModelRegistryError)); + typeof e === 'object' && e !== null && ['code', 'message'].every((key) => key in e); export const handleModelRegistryFailures = (promise: Promise): Promise => promise diff --git a/frontend/src/api/pipelines/errorUtils.ts b/frontend/src/api/pipelines/errorUtils.ts index 384e3d6d2f..4efaa5da7c 100644 --- a/frontend/src/api/pipelines/errorUtils.ts +++ b/frontend/src/api/pipelines/errorUtils.ts @@ -14,10 +14,12 @@ type ResultErrorKF = { }; const isErrorKF = (e: unknown): e is ErrorKF => - ['error', 'code', 'message'].every((key) => key in (e as ErrorKF)); + typeof e === 'object' && e !== null && ['error', 'code', 'message'].every((key) => key in e); const isErrorDetailsKF = (result: unknown): result is ResultErrorKF => - ['error_details', 'error_message'].every((key) => key in (result as ResultErrorKF)); + typeof result === 'object' && + result !== null && + ['error_details', 'error_message'].every((key) => key in result); export const handlePipelineFailures = (promise: Promise): Promise => promise diff --git a/frontend/src/api/prometheus/serving.ts b/frontend/src/api/prometheus/serving.ts index 9cf7f35188..102b389c2b 100644 --- a/frontend/src/api/prometheus/serving.ts +++ b/frontend/src/api/prometheus/serving.ts @@ -14,6 +14,7 @@ import { RefreshIntervalValue } from '~/concepts/metrics/const'; import useRefreshInterval from '~/utilities/useRefreshInterval'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { PROMETHEUS_BIAS_PATH } from '~/api/prometheus/const'; +import { EitherNotBoth } from '~/typeHelpers'; import useQueryRangeResourceData from './useQueryRangeResourceData'; import { defaultResponsePredicate, @@ -22,17 +23,26 @@ import { export const useModelServingMetrics = ( type: PerformanceMetricType, - queries: { [key in ModelMetricType]: string } | { [key in ServerMetricType]: string }, + queries: EitherNotBoth< + { [key in ModelMetricType]: string }, + { [key in ServerMetricType]: string } + >, timeframe: TimeframeTitle, lastUpdateTime: number, setLastUpdateTime: (time: number) => void, refreshInterval: RefreshIntervalTitle, namespace: string, ): { - data: Record< - ServerMetricType | ModelMetricType, - ContextResourceData - >; + data: { + [ServerMetricType.REQUEST_COUNT]: ContextResourceData; + [ServerMetricType.AVG_RESPONSE_TIME]: ContextResourceData; + [ServerMetricType.CPU_UTILIZATION]: ContextResourceData; + [ServerMetricType.MEMORY_UTILIZATION]: ContextResourceData; + [ModelMetricType.REQUEST_COUNT_FAILED]: ContextResourceData; + [ModelMetricType.REQUEST_COUNT_SUCCESS]: ContextResourceData; + [ModelMetricType.TRUSTY_AI_SPD]: ContextResourceData; + [ModelMetricType.TRUSTY_AI_DIR]: ContextResourceData; + }; refresh: () => void; } => { const [end, setEnd] = React.useState(lastUpdateTime); @@ -43,7 +53,7 @@ export const useModelServingMetrics = ( const serverRequestCount = useQueryRangeResourceData( performanceMetricsAreaAvailable && type === PerformanceMetricType.SERVER, - (queries as { [key in ServerMetricType]: string })[ServerMetricType.REQUEST_COUNT], + queries[ServerMetricType.REQUEST_COUNT] ?? '', end, timeframe, defaultResponsePredicate, @@ -53,7 +63,7 @@ export const useModelServingMetrics = ( const serverAverageResponseTime = useQueryRangeResourceData( performanceMetricsAreaAvailable && type === PerformanceMetricType.SERVER, - (queries as { [key in ServerMetricType]: string })[ServerMetricType.AVG_RESPONSE_TIME], + queries[ServerMetricType.AVG_RESPONSE_TIME] ?? '', end, timeframe, prometheusQueryRangeResponsePredicate, @@ -62,7 +72,7 @@ export const useModelServingMetrics = ( const serverCPUUtilization = useQueryRangeResourceData( performanceMetricsAreaAvailable && type === PerformanceMetricType.SERVER, - (queries as { [key in ServerMetricType]: string })[ServerMetricType.CPU_UTILIZATION], + queries[ServerMetricType.CPU_UTILIZATION] ?? '', end, timeframe, defaultResponsePredicate, @@ -71,7 +81,7 @@ export const useModelServingMetrics = ( const serverMemoryUtilization = useQueryRangeResourceData( performanceMetricsAreaAvailable && type === PerformanceMetricType.SERVER, - (queries as { [key in ServerMetricType]: string })[ServerMetricType.MEMORY_UTILIZATION], + queries[ServerMetricType.MEMORY_UTILIZATION] ?? '', end, timeframe, defaultResponsePredicate, @@ -80,7 +90,7 @@ export const useModelServingMetrics = ( const modelRequestSuccessCount = useQueryRangeResourceData( performanceMetricsAreaAvailable && type === PerformanceMetricType.MODEL, - (queries as { [key in ModelMetricType]: string })[ModelMetricType.REQUEST_COUNT_SUCCESS], + queries[ModelMetricType.REQUEST_COUNT_SUCCESS] ?? '', end, timeframe, defaultResponsePredicate, @@ -89,7 +99,7 @@ export const useModelServingMetrics = ( const modelRequestFailedCount = useQueryRangeResourceData( performanceMetricsAreaAvailable && type === PerformanceMetricType.MODEL, - (queries as { [key in ModelMetricType]: string })[ModelMetricType.REQUEST_COUNT_FAILED], + queries[ModelMetricType.REQUEST_COUNT_FAILED] ?? '', end, timeframe, defaultResponsePredicate, @@ -98,7 +108,7 @@ export const useModelServingMetrics = ( const modelTrustyAISPD = useQueryRangeResourceData( biasMetricsAreaAvailable && type === PerformanceMetricType.MODEL, - (queries as { [key in ModelMetricType]: string })[ModelMetricType.TRUSTY_AI_SPD], + queries[ModelMetricType.TRUSTY_AI_SPD] ?? '', end, timeframe, prometheusQueryRangeResponsePredicate, @@ -108,7 +118,7 @@ export const useModelServingMetrics = ( const modelTrustyAIDIR = useQueryRangeResourceData( biasMetricsAreaAvailable && type === PerformanceMetricType.MODEL, - (queries as { [key in ModelMetricType]: string })[ModelMetricType.TRUSTY_AI_DIR], + queries[ModelMetricType.TRUSTY_AI_DIR] ?? '', end, timeframe, prometheusQueryRangeResponsePredicate, diff --git a/frontend/src/api/trustyai/errorUtils.ts b/frontend/src/api/trustyai/errorUtils.ts index 60fd07a7f6..6f3625f38e 100644 --- a/frontend/src/api/trustyai/errorUtils.ts +++ b/frontend/src/api/trustyai/errorUtils.ts @@ -9,8 +9,7 @@ type TrustyAIClientErrorViolation = { }; const isTrustyAIClientError = (e: unknown): e is TrustyAIClientError => - typeof e === 'object' && - ['title', 'status', 'violations'].every((key) => key in (e as TrustyAIClientError)); + typeof e === 'object' && e !== null && ['title', 'status', 'violations'].every((key) => key in e); export const handleTrustyAIFailures = (promise: Promise): Promise => promise.then((result) => { diff --git a/frontend/src/app/AppContext.ts b/frontend/src/app/AppContext.ts index bdc0141450..2b51e99c15 100644 --- a/frontend/src/app/AppContext.ts +++ b/frontend/src/app/AppContext.ts @@ -8,13 +8,7 @@ type AppContextProps = { storageClasses: StorageClassKind[]; }; -const defaultAppContext: AppContextProps = { - buildStatuses: [], - // At runtime dashboardConfig is never null -- DO NOT DO THIS usually - dashboardConfig: null as unknown as DashboardConfigKind, - storageClasses: [] as StorageClassKind[], -}; - -export const AppContext = React.createContext(defaultAppContext); +// eslint-disable-next-line @typescript-eslint/consistent-type-assertions +export const AppContext = React.createContext({} as AppContextProps); export const useAppContext = (): AppContextProps => React.useContext(AppContext); diff --git a/frontend/src/components/DocCardBadges.tsx b/frontend/src/components/DocCardBadges.tsx index 9a82f54235..e0270402dd 100644 --- a/frontend/src/components/DocCardBadges.tsx +++ b/frontend/src/components/DocCardBadges.tsx @@ -5,7 +5,7 @@ import { QuickStartContext, QuickStartContextValues } from '@patternfly/quicksta import { OdhDocument, OdhDocumentType } from '~/types'; import { getQuickStartCompletionStatus, CompletionStatusEnum } from '~/utilities/quickStartUtils'; import { DOC_TYPE_TOOLTIPS } from '~/utilities/const'; -import { getLabelColorForDocType, getDuration } from '~/utilities/utils'; +import { getLabelColorForDocType, getDuration, asEnumMember } from '~/utilities/utils'; import { DOC_TYPE_LABEL } from '~/pages/learningCenter/const'; import './OdhCard.scss'; @@ -19,7 +19,7 @@ const DocCardBadges: React.FC = ({ odhDoc }) => { const [completionStatus, setCompletionStatus] = React.useState< CompletionStatusEnum | undefined >(); - const docType = odhDoc.spec.type as OdhDocumentType; + const docType = asEnumMember(odhDoc.spec.type, OdhDocumentType) ?? OdhDocumentType.Documentation; const docName = odhDoc.metadata.name; const duration = odhDoc.spec.durationMinutes; diff --git a/frontend/src/components/FilterSidePanelCategoryItem.tsx b/frontend/src/components/FilterSidePanelCategoryItem.tsx index 1c01ec741b..9dcb06ddd8 100644 --- a/frontend/src/components/FilterSidePanelCategoryItem.tsx +++ b/frontend/src/components/FilterSidePanelCategoryItem.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { css } from '@patternfly/react-styles'; -import { Checkbox } from '@patternfly/react-core'; +import { Checkbox, CheckboxProps } from '@patternfly/react-core'; -export interface FilterSidePanelCategoryItemProps extends React.HTMLProps { +export interface FilterSidePanelCategoryItemProps { id: string; /** Children nodes */ children: React.ReactNode; @@ -13,7 +13,7 @@ export interface FilterSidePanelCategoryItemProps extends React.HTMLProps) => void; + onChange?: CheckboxProps['onChange']; /** Flag to show if the Filter Item Checkbox is checked. */ checked?: boolean; /** Title of the checkbox */ @@ -27,7 +27,7 @@ const FilterSidePanelCategoryItem: React.FunctionComponent = ({ odhDoc, favorite, updateFav }; const renderTypeBadge = () => { - const docType = odhDoc.spec.type as OdhDocumentType; + const docType = + asEnumMember(odhDoc.spec.type, OdhDocumentType) ?? OdhDocumentType.Documentation; const typeBadgeClasses = classNames('odh-list-item__partner-badge odh-m-doc', { 'odh-m-documentation': docType === OdhDocumentType.Documentation, 'odh-m-tutorial': docType === OdhDocumentType.Tutorial, diff --git a/frontend/src/components/ToastNotification.tsx b/frontend/src/components/ToastNotification.tsx index 4f2a2299ab..b0a945705b 100644 --- a/frontend/src/components/ToastNotification.tsx +++ b/frontend/src/components/ToastNotification.tsx @@ -3,6 +3,7 @@ import { Alert, AlertActionCloseButton, AlertVariant } from '@patternfly/react-c import { AppNotification } from '~/redux/types'; import { ackNotification, hideNotification } from '~/redux/actions/actions'; import { useAppDispatch } from '~/redux/hooks'; +import { asEnumMember } from '~/utilities/utils'; const TOAST_NOTIFICATION_TIMEOUT = 8 * 1000; @@ -36,7 +37,7 @@ const ToastNotification: React.FC = ({ notification }) = return ( dispatch(ackNotification(notification))} /> diff --git a/frontend/src/components/browserStorage/BrowserStorageContext.tsx b/frontend/src/components/browserStorage/BrowserStorageContext.tsx index f64d105113..ff903a9cfa 100644 --- a/frontend/src/components/browserStorage/BrowserStorageContext.tsx +++ b/frontend/src/components/browserStorage/BrowserStorageContext.tsx @@ -48,6 +48,7 @@ export const useBrowserStorage = ( [isSessionStorage, jsonify, setJSONValue, setStringValue, storageKey], ); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return [(getValue(storageKey, jsonify, isSessionStorage) as T) ?? defaultValue, setValue]; }; diff --git a/frontend/src/components/table/useTableColumnSort.ts b/frontend/src/components/table/useTableColumnSort.ts index ce179df0a0..56c4519d68 100644 --- a/frontend/src/components/table/useTableColumnSort.ts +++ b/frontend/src/components/table/useTableColumnSort.ts @@ -105,7 +105,9 @@ const useTableColumnSort = ( return 0; } + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const dataValueA = a[columnField.field as keyof T]; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const dataValueB = b[columnField.field as keyof T]; if (typeof dataValueA === 'string' && typeof dataValueB === 'string') { return dataValueA.localeCompare(dataValueB); diff --git a/frontend/src/concepts/areas/utils.ts b/frontend/src/concepts/areas/utils.ts index 2957080141..5926dc80a3 100644 --- a/frontend/src/concepts/areas/utils.ts +++ b/frontend/src/concepts/areas/utils.ts @@ -15,8 +15,7 @@ const getFlags = (dashboardConfigSpec: DashboardConfigKind['spec']): FlagState = typeof value === 'boolean'; return { - ...Object.keys(flags).reduce((acc, key) => { - const value = flags[key as FeatureFlag]; + ...Object.entries(flags).reduce((acc, [key, value]) => { if (isFeatureFlag(key, value)) { acc[key] = key.startsWith('disable') ? !value : value; } diff --git a/frontend/src/concepts/dashboard/DashboardSearchField.tsx b/frontend/src/concepts/dashboard/DashboardSearchField.tsx index 18d45e1da6..44d96fa0a0 100644 --- a/frontend/src/concepts/dashboard/DashboardSearchField.tsx +++ b/frontend/src/concepts/dashboard/DashboardSearchField.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { InputGroup, SearchInput, InputGroupItem } from '@patternfly/react-core'; import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; +import { asEnumMember } from '~/utilities/utils'; // List all the possible search fields here export enum SearchType { @@ -48,7 +49,10 @@ const DashboardSearchField: React.FC = ({ }))} value={searchType} onChange={(key) => { - onSearchTypeChange(key as SearchType); + const enumMember = asEnumMember(key, SearchType); + if (enumMember !== null) { + onSearchTypeChange(enumMember); + } }} icon={icon} /> diff --git a/frontend/src/concepts/distributedWorkloads/utils.tsx b/frontend/src/concepts/distributedWorkloads/utils.tsx index db466618d7..29a4de3785 100644 --- a/frontend/src/concepts/distributedWorkloads/utils.tsx +++ b/frontend/src/concepts/distributedWorkloads/utils.tsx @@ -34,6 +34,7 @@ import { convertToUnit, } from '~/utilities/valueUnits'; import { WorkloadWithUsage } from '~/api'; +import { isEnumMember } from '~/utilities/utils'; export enum WorkloadStatusType { Pending = 'Pending', @@ -166,7 +167,7 @@ export const getWorkloadName = (workload: WorkloadKind): string => workload.metadata?.name || 'Unnamed'; export const isKnownWorkloadOwnerType = (s: string): s is WorkloadOwnerType => - (Object.values(WorkloadOwnerType) as string[]).includes(s); + isEnumMember(s, WorkloadOwnerType); export const getWorkloadOwner = ( workload: WorkloadKind, diff --git a/frontend/src/concepts/docResources/docUtils.ts b/frontend/src/concepts/docResources/docUtils.ts index e66d889d94..1abeb77360 100644 --- a/frontend/src/concepts/docResources/docUtils.ts +++ b/frontend/src/concepts/docResources/docUtils.ts @@ -30,6 +30,7 @@ export const getQuickStartDocs = ( // Get doc cards for the quick starts const docs: OdhDocument[] = quickStarts.map( (quickStart) => + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions _.merge({}, quickStart, { spec: { type: OdhDocumentType.QuickStart, diff --git a/frontend/src/concepts/metrics/utils.ts b/frontend/src/concepts/metrics/utils.ts index 1f44bc91cf..c88e5d2592 100644 --- a/frontend/src/concepts/metrics/utils.ts +++ b/frontend/src/concepts/metrics/utils.ts @@ -1,12 +1,12 @@ import { SelectOptionObject } from '@patternfly/react-core/deprecated'; import { TimeframeTitle, RefreshIntervalTitle } from '~/concepts/metrics/types'; +import { isEnumMember } from '~/utilities/utils'; export const isTimeframeTitle = ( timeframe: string | SelectOptionObject, -): timeframe is TimeframeTitle => - Object.values(TimeframeTitle).includes(timeframe as TimeframeTitle); +): timeframe is TimeframeTitle => isEnumMember(timeframe.toString(), TimeframeTitle); export const isRefreshIntervalTitle = ( refreshInterval: string | SelectOptionObject, ): refreshInterval is RefreshIntervalTitle => - Object.values(RefreshIntervalTitle).includes(refreshInterval as RefreshIntervalTitle); + isEnumMember(refreshInterval.toString(), RefreshIntervalTitle); diff --git a/frontend/src/concepts/modelRegistry/context/ModelRegistryContext.tsx b/frontend/src/concepts/modelRegistry/context/ModelRegistryContext.tsx index eb3bdce52d..dfde7ebcc8 100644 --- a/frontend/src/concepts/modelRegistry/context/ModelRegistryContext.tsx +++ b/frontend/src/concepts/modelRegistry/context/ModelRegistryContext.tsx @@ -28,6 +28,7 @@ export const ModelRegistryContext = React.createContext undefined, refreshState: async () => undefined, diff --git a/frontend/src/concepts/pipelines/content/PipelineAndVersionContext.tsx b/frontend/src/concepts/pipelines/content/PipelineAndVersionContext.tsx index eed1a71d94..858635fbca 100644 --- a/frontend/src/concepts/pipelines/content/PipelineAndVersionContext.tsx +++ b/frontend/src/concepts/pipelines/content/PipelineAndVersionContext.tsx @@ -77,12 +77,13 @@ const PipelineAndVersionContextProvider: React.FC ({ pipelines: selectedPipelines, - versions: (Object.values(selectedVersions) as SelectedVersion[]) - .map((selectedVersion) => - selectedVersion.versions.map((version) => ({ - pipelineName: selectedVersion.pipelineName, - version, - })), + versions: Object.values(selectedVersions) + .map( + (selectedVersion) => + selectedVersion?.versions.map((version) => ({ + pipelineName: selectedVersion.pipelineName, + version, + })) ?? [], ) .flat() .filter((selection) => { diff --git a/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx b/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx index 6a2c9b1fa4..894e2d599f 100644 --- a/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx +++ b/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx @@ -14,16 +14,19 @@ import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { useIsAreaAvailable, SupportedArea } from '~/concepts/areas'; import { MAX_STORAGE_OBJECT_SIZE, fetchStorageObjectSize } from '~/services/storageService'; import { bytesAsRoundedGiB } from '~/utilities/number'; +import { ArtifactType } from '~/concepts/pipelines/kfTypes'; import { extractS3UriComponents, getArtifactUrlFromUri } from './utils'; interface ArtifactUriLinkProps { uri: string; + type: string; } -export const ArtifactUriLink: React.FC = ({ uri }) => { +export const ArtifactUriLink: React.FC = ({ uri, type }) => { const { namespace } = usePipelinesAPI(); const isS3EndpointAvailable = useIsAreaAvailable(SupportedArea.S3_ENDPOINT).status; const [size, setSize] = React.useState(null); + const isClassificationMetrics = type === ArtifactType.CLASSIFICATION_METRICS; const url = React.useMemo(() => { if (!uri || !isS3EndpointAvailable) { @@ -41,7 +44,7 @@ export const ArtifactUriLink: React.FC = ({ uri }) => { return getArtifactUrlFromUri(uri, namespace); }, [isS3EndpointAvailable, namespace, uri]); - if (!url) { + if (!url || isClassificationMetrics) { return uri; } diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect.tsx index 2b15849d79..97c5f0217c 100644 --- a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect.tsx +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect.tsx @@ -49,10 +49,12 @@ export const PipelineRunArtifactSelect = ({ isOpen={isOpen} selected={selectedItemTitles} onSelect={(_event, value) => { - if (selectedItemTitles.includes(value as string)) { - setSelectedItemTitles(selectedItemTitles.filter((id) => id !== value)); - } else { - setSelectedItemTitles([...selectedItemTitles, value as string]); + if (typeof value === 'string') { + if (selectedItemTitles.includes(value)) { + setSelectedItemTitles(selectedItemTitles.filter((id) => id !== value)); + } else { + setSelectedItemTitles([...selectedItemTitles, value]); + } } }} onOpenChange={(nextOpen: boolean) => setIsOpen(nextOpen)} diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare.tsx index 60ec951f71..b4c3b7694c 100644 --- a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare.tsx +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare.tsx @@ -49,13 +49,16 @@ const ConfusionMatrixCompare: React.FC = ({ const { configMap, runMap } = React.useMemo( () => - fullArtifactPaths.reduce( + fullArtifactPaths.reduce<{ + runMap: Record; + configMap: Record; + }>( (acc, fullPath) => { const customProperties = fullPath.linkedArtifact.artifact.getCustomPropertiesMap(); const data = customProperties.get('confusionMatrix')?.getStructValue()?.toJavaScript(); if (data) { - const confusionMatrixData = data.struct as unknown; + const confusionMatrixData = data.struct; if (isConfusionMatrix(confusionMatrixData)) { const runId = fullPath.run.run_id; const title = getFullArtifactPathLabel(fullPath); @@ -78,10 +81,7 @@ const ConfusionMatrixCompare: React.FC = ({ return acc; }, - { - runMap: {} as Record, - configMap: {} as Record, - }, + { runMap: {}, configMap: {} }, ), [fullArtifactPaths], ); diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/utils.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/utils.ts index 47fea29c76..2891587bd9 100644 --- a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/utils.ts +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/utils.ts @@ -1,8 +1,12 @@ import { ConfusionMatrixInput } from '~/concepts/pipelines/content/artifacts/charts/confusionMatrix/types'; export const isConfusionMatrix = (obj: unknown): obj is ConfusionMatrixInput => { - const matrix = obj as ConfusionMatrixInput; + const matrix = obj; return ( + typeof matrix === 'object' && + matrix !== null && + 'annotationSpecs' in matrix && + 'rows' in matrix && Array.isArray(matrix.annotationSpecs) && matrix.annotationSpecs.every( (annotationSpec) => @@ -10,7 +14,8 @@ export const isConfusionMatrix = (obj: unknown): obj is ConfusionMatrixInput => ) && Array.isArray(matrix.rows) && matrix.rows.every( - (row) => Array.isArray(row.row) && row.row.every((value) => typeof value === 'number'), + (row) => + Array.isArray(row.row) && row.row.every((value: unknown) => typeof value === 'number'), ) ); }; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/utils.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/utils.ts index c3bf9da5d8..74582cf10e 100644 --- a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/utils.ts +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/utils.ts @@ -2,14 +2,15 @@ import { JavaScriptValue } from 'google-protobuf/google/protobuf/struct_pb'; import { ROCCurveConfig } from '~/concepts/pipelines/content/artifacts/charts/ROCCurve'; import { ConfidenceMetric } from './types'; -export const isConfidenceMetric = (obj: JavaScriptValue): obj is ConfidenceMetric => { - const metric = obj as ConfidenceMetric; - return ( - typeof metric.confidenceThreshold === 'number' && - typeof metric.falsePositiveRate === 'number' && - typeof metric.recall === 'number' - ); -}; +export const isConfidenceMetric = (obj: JavaScriptValue): obj is ConfidenceMetric => + typeof obj === 'object' && + obj !== null && + 'confidenceThreshold' in obj && + 'falsePositiveRate' in obj && + 'recall' in obj && + typeof obj.confidenceThreshold === 'number' && + typeof obj.falsePositiveRate === 'number' && + typeof obj.recall === 'number'; export const buildRocCurveConfig = ( confidenceMetricsArray: ConfidenceMetric[], diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/utils.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/utils.ts index 38a034f605..b5907e164e 100644 --- a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/utils.ts +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/utils.ts @@ -58,7 +58,7 @@ export const getRunArtifacts = (mlmdPackages: PipelineRunRelatedMlmd[]): RunArti linkedArtifacts.push({ event, artifact, - } as LinkedArtifact); + }); } else { throw new Error(`The artifact with the following ID was not found: ${artifactId}`); } @@ -66,12 +66,12 @@ export const getRunArtifacts = (mlmdPackages: PipelineRunRelatedMlmd[]): RunArti return { execution, linkedArtifacts, - } as ExecutionArtifact; + }; }); return { run: mlmdPackage.run, executionArtifacts, - } as RunArtifact; + }; }); export const filterRunArtifactsByType = ( @@ -102,14 +102,14 @@ export const filterRunArtifactsByType = ( typeExecutions.push({ execution: e.execution, linkedArtifacts: typeArtifacts, - } as ExecutionArtifact); + }); } } if (typeExecutions.length > 0) { typeRuns.push({ run: runArtifact.run, executionArtifacts: typeExecutions, - } as RunArtifact); + }); } } return typeRuns; diff --git a/frontend/src/concepts/pipelines/content/configurePipelinesServer/ConfigurePipelinesServerModal.tsx b/frontend/src/concepts/pipelines/content/configurePipelinesServer/ConfigurePipelinesServerModal.tsx index 252de75602..4a16b8725c 100644 --- a/frontend/src/concepts/pipelines/content/configurePipelinesServer/ConfigurePipelinesServerModal.tsx +++ b/frontend/src/concepts/pipelines/content/configurePipelinesServer/ConfigurePipelinesServerModal.tsx @@ -10,7 +10,6 @@ import { PipelinesDatabaseSection } from './PipelinesDatabaseSection'; import { ObjectStorageSection } from './ObjectStorageSection'; import { DATABASE_CONNECTION_FIELDS, - DatabaseConnectionKeys, EMPTY_DATABASE_CONNECTION, ExternalDatabaseSecret, } from './const'; @@ -48,7 +47,7 @@ export const ConfigurePipelinesServerModal: React.FC DATABASE_CONNECTION_FIELDS.filter((field) => field.isRequired) .map((field) => field.key) - .includes(key as DatabaseConnectionKeys) + .includes(key) ? !!value : true, ); diff --git a/frontend/src/concepts/pipelines/content/configurePipelinesServer/__tests__/utils.spec.ts b/frontend/src/concepts/pipelines/content/configurePipelinesServer/__tests__/utils.spec.ts index 82da6d349d..b0b20d70b6 100644 --- a/frontend/src/concepts/pipelines/content/configurePipelinesServer/__tests__/utils.spec.ts +++ b/frontend/src/concepts/pipelines/content/configurePipelinesServer/__tests__/utils.spec.ts @@ -71,6 +71,16 @@ describe('configure pipeline server utils', () => { expect(spec.objectStorage.externalStorage?.host).toBe('s3.amazonaws.com'); }); + it('should cleanup S3 endpoint host', () => { + const secretsResponse = createSecretsResponse(); + const config = createPipelineServerConfig(); + config.objectStorage.newValue = [ + { key: AwsKeys.S3_ENDPOINT, value: 'https://s3.amazonaws.com/' }, + ]; + const spec = createDSPipelineResourceSpec(config, secretsResponse); + expect(spec.objectStorage.externalStorage?.host).toBe('s3.amazonaws.com'); + }); + it('should parse S3 endpoint without scheme', () => { const secretsResponse = createSecretsResponse(); const config = createPipelineServerConfig(); diff --git a/frontend/src/concepts/pipelines/content/configurePipelinesServer/utils.ts b/frontend/src/concepts/pipelines/content/configurePipelinesServer/utils.ts index 41584d343f..4741b6bba0 100644 --- a/frontend/src/concepts/pipelines/content/configurePipelinesServer/utils.ts +++ b/frontend/src/concepts/pipelines/content/configurePipelinesServer/utils.ts @@ -108,7 +108,7 @@ export const createDSPipelineResourceSpec = ( dspVersion: 'v2', objectStorage: { externalStorage: { - host: externalStorageHost, + host: cleanupEndpointHost(externalStorageHost), scheme: externalStorageScheme || 'https', bucket: awsRecord.AWS_S3_BUCKET || '', region: externalStorageRegion, @@ -157,3 +157,5 @@ export const getLabelName = (index: string): string => { const field = PIPELINE_AWS_FIELDS.find((currentField) => currentField.key === index); return field ? field.label : ''; }; + +const cleanupEndpointHost = (endpoint: string): string => endpoint.replace(/\/$/, '') || ''; diff --git a/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx b/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx index faf0c58670..7996a4a868 100644 --- a/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx @@ -21,7 +21,7 @@ const CloneRunPage: React.FC = ({ breadcrumbPath, contextPath }) => { title={title} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath(runType)} {run ? `Duplicate of ${run.display_name}` : 'Duplicate'} diff --git a/frontend/src/concepts/pipelines/content/createRun/CreateRunPage.tsx b/frontend/src/concepts/pipelines/content/createRun/CreateRunPage.tsx index 3930ac2091..1ce7f64b25 100644 --- a/frontend/src/concepts/pipelines/content/createRun/CreateRunPage.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/CreateRunPage.tsx @@ -16,7 +16,9 @@ const CreateRunPage: React.FC = ({ breadcrumbPath, contextPath }) => title={title} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath( + runType === PipelineRunType.SCHEDULED ? PipelineRunType.SCHEDULED : undefined, + )} {title} } diff --git a/frontend/src/concepts/pipelines/content/createRun/RunPage.tsx b/frontend/src/concepts/pipelines/content/createRun/RunPage.tsx index 8761f6f942..e9b928131c 100644 --- a/frontend/src/concepts/pipelines/content/createRun/RunPage.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/RunPage.tsx @@ -118,7 +118,7 @@ const RunPage: React.FC = ({ cloneRun, contextPath, testId }) => { > diff --git a/frontend/src/concepts/pipelines/content/createRun/contentSections/ParamsSection/NumberInputParam.tsx b/frontend/src/concepts/pipelines/content/createRun/contentSections/ParamsSection/NumberInputParam.tsx index 04d1b98a6f..fb44f00307 100644 --- a/frontend/src/concepts/pipelines/content/createRun/contentSections/ParamsSection/NumberInputParam.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/contentSections/ParamsSection/NumberInputParam.tsx @@ -1,5 +1,5 @@ import { TextInput } from '@patternfly/react-core'; -import React from 'react'; +import React, { useRef } from 'react'; import NumberInputWrapper from '~/components/NumberInputWrapper'; import { InputParamProps } from './types'; @@ -15,16 +15,24 @@ export const NumberInputParam: React.FC = ({ const [value, setValue] = React.useState( inputProps.value !== '' ? Number(inputProps.value) : '', ); + const isDefault = useRef(true); if (isFloat) { + // if the default value is a whole number, display it as x.0 + const displayValue = + typeof value === 'number' && Number.isInteger(value) && isDefault.current + ? value.toFixed(1) + : value; + return ( { + isDefault.current = false; setValue(typeof newValue === 'string' ? parseFloat(newValue) : newValue); onChange(event, newValue); }} diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsTitle.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsTitle.tsx index 7a02caa2df..8bc8db18ab 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsTitle.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsTitle.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Label, Split, SplitItem } from '@patternfly/react-core'; +import { Label, Split, SplitItem, Truncate } from '@patternfly/react-core'; import { PipelineRunKFv2, StorageStateKF } from '~/concepts/pipelines/kfTypes'; import { computeRunStatus } from '~/concepts/pipelines/content/utils'; import PipelineRunTypeLabel from '~/concepts/pipelines/content/PipelineRunTypeLabel'; @@ -22,7 +22,10 @@ const PipelineDetailsTitle: React.FC = ({ return ( <> - {run.display_name} + + {/* TODO: Remove the custom className after upgrading to PFv6 */} + + {pipelineRunLabel && ( diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx index 78bf3552b5..915fcdec68 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx @@ -69,7 +69,7 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = - {breadcrumbPath} + {breadcrumbPath()} {title} } @@ -97,14 +97,30 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = - {breadcrumbPath} - {pipeline?.display_name || 'Loading...'} + {breadcrumbPath()} + + {/* TODO: Remove the custom className after upgrading to PFv6 */} + + - + {/* TODO: Remove the custom className after upgrading to PFv6 */} + } - title={} + title={ + + } {...(pipelineVersion && { description: ( @@ -128,7 +132,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, loadError={error} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath(runType)} {version ? ( - {version.display_name} + {/* TODO: Remove the custom className after upgrading to PFv6 */} + ) : ( 'Loading...' )} - + {/* TODO: Remove the custom className after upgrading to PFv6 */} + } diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeInputOutputTab.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeInputOutputTab.tsx index 3bd6701fc4..6ba2dec35d 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeInputOutputTab.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/SelectedNodeInputOutputTab.tsx @@ -48,6 +48,9 @@ const SelectedNodeInputOutputTab: React.FC = ({ switch (type) { case InputDefinitionParameterType.DOUBLE: + return numberValue && Number.isInteger(numberValue) + ? numberValue.toFixed(1) + : numberValue; case InputDefinitionParameterType.INTEGER: return numberValue; case InputDefinitionParameterType.BOOLEAN: diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx index cda7ec68e1..8ec39e35e9 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx @@ -10,7 +10,6 @@ import { DescriptionListGroup, DescriptionListTerm, DescriptionListDescription, - StackItem, } from '@patternfly/react-core'; import { Artifact } from '~/third_party/mlmd'; @@ -21,7 +20,6 @@ import PipelinesTableRowTime from '~/concepts/pipelines/content/tables/Pipelines import PipelineRunDrawerRightContent from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { ArtifactUriLink } from '~/concepts/pipelines/content/artifacts/ArtifactUriLink'; -import ArtifactPreview from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/ArtifactPreview'; type ArtifactNodeDetailsProps = Pick< React.ComponentProps, @@ -90,14 +88,7 @@ export const ArtifactNodeDetails: React.FC = ({ {artifactName} - - - - - - - - + diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx index a07f9a8834..abb5335da2 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx @@ -94,7 +94,7 @@ const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ loadError={error} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath(PipelineRunType.SCHEDULED)} {version ? ( {version.display_name} diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/ArtifactPreview.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/ArtifactPreview.tsx deleted file mode 100644 index 2392ff8457..0000000000 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/ArtifactPreview.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import { Bullseye, CodeBlock, CodeBlockCode, Spinner } from '@patternfly/react-core'; -import { useIsAreaAvailable, SupportedArea } from '~/concepts/areas'; -import { usePipelinesAPI } from '~/concepts/pipelines/context'; -import { Artifact } from '~/third_party/mlmd'; -import { extractS3UriComponents } from '~/concepts/pipelines/content/artifacts/utils'; -import { fetchStorageObject } from '~/services/storageService'; - -type ArtifactPreviewProps = { - artifact: Artifact; - maxBytes?: number; - maxLines?: number; -}; - -const ArtifactPreview: React.FC = ({ - artifact, - maxBytes = 255, - maxLines = 4, -}) => { - const isS3EndpointAvailable = useIsAreaAvailable(SupportedArea.S3_ENDPOINT).status; - const { namespace } = usePipelinesAPI(); - const [preview, setPreview] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(false); - - React.useEffect(() => { - const uri = artifact.getUri(); - if (!uri || !isS3EndpointAvailable) { - return; - } - - setPreview(null); - const uriComponents = extractS3UriComponents(uri); - if (!uriComponents) { - return; - } - setIsLoading(true); - fetchStorageObject(namespace, uriComponents.path, maxBytes) - .catch(() => null) - .then((text) => setPreview(text)) - .finally(() => setIsLoading(false)); - }, [artifact, isS3EndpointAvailable, maxBytes, namespace]); - - if (isLoading) { - return ( - - - - ); - } - - if (!preview) { - return null; - } - - // Try to parse the preview as JSON - let code = preview; - try { - code = JSON.parse(preview); - code = JSON.stringify(code, null, 2); - } catch { - // ignore - } - - code = code.split('\n').slice(0, maxLines).join('\n').trim(); - code = `${code}...`; - - return ( - - {code} - - ); -}; - -export default ArtifactPreview; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputOutput.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputOutput.tsx index 911ae667aa..67ef8c9c89 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputOutput.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsInputOutput.tsx @@ -4,7 +4,6 @@ import TaskDetailsSection from '~/concepts/pipelines/content/pipelinesDetails/ta import TaskDetailsPrintKeyValues from '~/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues'; import { PipelineTaskArtifact } from '~/concepts/pipelines/topology'; import { ArtifactUriLink } from '~/concepts/pipelines/content/artifacts/ArtifactUriLink'; -import ArtifactPreview from './ArtifactPreview'; type TaskDetailsInputOutputProps = { type: 'Input' | 'Output'; @@ -28,8 +27,7 @@ const TaskDetailsInputOutput: React.FC = ({ if (artifact) { return { label: artifactInputOutput.label, - value: , - preview: , + value: , }; } diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues.tsx index b5935bc144..5ecd331b4b 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/taskDetails/TaskDetailsPrintKeyValues.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Grid, GridItem, Truncate } from '@patternfly/react-core'; type TaskDetailsPrintKeyValuesProps = { - items: { label: string; value: React.ReactNode; preview?: React.ReactNode }[]; + items: { label: string; value: React.ReactNode }[]; }; const TaskDetailsPrintKeyValues: React.FC = ({ items }) => ( @@ -15,7 +15,6 @@ const TaskDetailsPrintKeyValues: React.FC = ({ i {result.value} - {result.preview && {result.preview}} ))} diff --git a/frontend/src/concepts/pipelines/content/tables/PipelineFilterBar.tsx b/frontend/src/concepts/pipelines/content/tables/PipelineFilterBar.tsx index 16bdea92fe..81c57a2034 100644 --- a/frontend/src/concepts/pipelines/content/tables/PipelineFilterBar.tsx +++ b/frontend/src/concepts/pipelines/content/tables/PipelineFilterBar.tsx @@ -41,11 +41,13 @@ export function FilterToolbar({ testId = 'filter-toolbar', ...toolbarGroupProps }: ToolbarFilterProps): React.JSX.Element { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const keys = Object.keys(filterOptions) as Array; const [open, setOpen] = React.useState(false); const [currentFilterType, setCurrentFilterType] = React.useState(keys[0]); const isToolbarChip = (v: unknown): v is ToolbarChip & { key: T } => - !!v && Object.keys(v as ToolbarChip).every((k) => ['key', 'node'].includes(k)); + !!v && Object.keys(v).every((k) => ['key', 'node'].includes(k)); + const filterItem = filterData[currentFilterType]; return ( <> @@ -111,9 +113,7 @@ export function FilterToolbar({ {filterOptionRenders[currentFilterType]({ onChange: (value, label) => onFilterUpdate(currentFilterType, label && value ? { label, value } : value), - ...(typeof filterData[currentFilterType] === 'string' - ? { value: filterData[currentFilterType] as string } - : (filterData[currentFilterType] as { label: string; value: string })), + ...(typeof filterItem === 'string' ? { value: filterItem } : filterItem), })} diff --git a/frontend/src/concepts/pipelines/content/tables/experiment/ExperimentTableRow.tsx b/frontend/src/concepts/pipelines/content/tables/experiment/ExperimentTableRow.tsx index 1ca7ac4cfb..dc1a4e31c4 100644 --- a/frontend/src/concepts/pipelines/content/tables/experiment/ExperimentTableRow.tsx +++ b/frontend/src/concepts/pipelines/content/tables/experiment/ExperimentTableRow.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; - import { ActionsColumn, IAction, Td, Tr } from '@patternfly/react-table'; - +import { Truncate } from '@patternfly/react-core'; import { ExperimentKFv2, StorageStateKF } from '~/concepts/pipelines/kfTypes'; import { CheckboxTd } from '~/components/table'; import { experimentRunsRoute } from '~/routes'; @@ -36,7 +35,8 @@ const ExperimentTableRow: React.FC = ({ }`} state={{ experiment }} > - {experiment.display_name} + {/* TODO: Remove the custom className after upgrading to PFv6 */} + {experiment.description} diff --git a/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx b/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx index 4114e00370..2cd996c41a 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Td, Tbody, Tr, ActionsColumn } from '@patternfly/react-table'; +import { Td, Tbody, Tr, ActionsColumn, TableText } from '@patternfly/react-table'; import { useNavigate } from 'react-router-dom'; import { Skeleton } from '@patternfly/react-core'; import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; @@ -84,7 +84,7 @@ const PipelinesTableRow: React.FC = ({ /> {pipeline.display_name}} description={pipeline.description} descriptionAsMarkdown /> diff --git a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRow.tsx b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRow.tsx index c86d4b4dd3..e3750cb67f 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRow.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRow.tsx @@ -22,6 +22,7 @@ import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { ArchiveRunModal } from '~/pages/pipelines/global/runs/ArchiveRunModal'; import PipelineRunTableRowExperiment from '~/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRowExperiment'; import { useContextExperimentArchived } from '~/pages/pipelines/global/experiments/ExperimentRunsContext'; +import { getDashboardMainContainer } from '~/utilities/utils'; type PipelineRunTableRowProps = { checkboxProps: Omit, 'id'>; @@ -161,7 +162,11 @@ const PipelineRunTableRow: React.FC = ({ {customCells} {hasRowActions && ( - + = ({ - {version.display_name} - + + + {version.display_name} + + } description={version.description} descriptionAsMarkdown diff --git a/frontend/src/concepts/pipelines/content/tables/usePipelineFilter.ts b/frontend/src/concepts/pipelines/content/tables/usePipelineFilter.ts index 9a1cd1031d..70235f7631 100644 --- a/frontend/src/concepts/pipelines/content/tables/usePipelineFilter.ts +++ b/frontend/src/concepts/pipelines/content/tables/usePipelineFilter.ts @@ -14,13 +14,11 @@ export enum FilterOptions { PIPELINE_VERSION = 'pipeline_version', } -export const getDataValue = ( - data: R, -): string | undefined => { - if (typeof data === 'string') { +export const getDataValue = (data: string | { value: string } | undefined): string | undefined => { + if (typeof data === 'string' || typeof data === 'undefined') { return data; } - return (data as { label: string; value: string } | undefined)?.value; + return data.value; }; const defaultFilterData: FilterProps['filterData'] = { diff --git a/frontend/src/concepts/pipelines/content/types.ts b/frontend/src/concepts/pipelines/content/types.ts index b3c90ac2ef..039e040579 100644 --- a/frontend/src/concepts/pipelines/content/types.ts +++ b/frontend/src/concepts/pipelines/content/types.ts @@ -1,8 +1,9 @@ import * as React from 'react'; import { BreadcrumbItem } from '@patternfly/react-core'; +import { PipelineRunType } from '~/pages/pipelines/global/runs'; export type PathProps = { - breadcrumbPath: React.ReactElement[]; + breadcrumbPath: (runType?: PipelineRunType | null) => React.ReactElement[]; contextPath?: string; }; diff --git a/frontend/src/concepts/pipelines/context/MlmdListContext.tsx b/frontend/src/concepts/pipelines/context/MlmdListContext.tsx index 159e7ff51a..4ddeb44524 100644 --- a/frontend/src/concepts/pipelines/context/MlmdListContext.tsx +++ b/frontend/src/concepts/pipelines/context/MlmdListContext.tsx @@ -16,6 +16,7 @@ export interface MlmdListContextProps { setOrderBy: (orderBy: MlmdOrderBy | undefined) => void; } +// eslint-disable-next-line @typescript-eslint/consistent-type-assertions const MlmdListContext = React.createContext({} as MlmdListContextProps); export const MlmdListContextProvider: React.FC = ({ children }) => { diff --git a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx index 8f9689f613..c58d9c74b8 100644 --- a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx +++ b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx @@ -51,6 +51,7 @@ const PipelinesContext = React.createContext({ serverTimedOut: false, ignoreTimedOut: () => undefined, namespace: '', + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions project: null as unknown as ProjectKind, refreshState: async () => undefined, refreshAPIState: () => undefined, @@ -58,7 +59,9 @@ const PipelinesContext = React.createContext({ loading: false, data: null, }), + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions apiState: { apiAvailable: false, api: null as unknown as PipelineAPIState['api'] }, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions metadataStoreServiceClient: null as unknown as MetadataStoreServicePromiseClient, }); @@ -106,7 +109,7 @@ export const PipelineContextProvider = conditionalArea {}); enableDevTools([client]); } diff --git a/frontend/src/concepts/pipelines/utils.ts b/frontend/src/concepts/pipelines/utils.ts index 006bb883f9..e674ecae2d 100644 --- a/frontend/src/concepts/pipelines/utils.ts +++ b/frontend/src/concepts/pipelines/utils.ts @@ -32,4 +32,4 @@ export const isGeneratedDSPAExternalStorageSecret = (name: string): boolean => export const isRunSchedule = ( resource: PipelineRunKFv2 | PipelineRunJobKFv2, -): resource is PipelineRunJobKFv2 => !!(resource as PipelineRunJobKFv2).trigger; +): resource is PipelineRunJobKFv2 => 'trigger' in resource; diff --git a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx index 520ad656da..b877763c03 100644 --- a/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx +++ b/frontend/src/concepts/topology/PipelineDefaultTaskGroup.tsx @@ -8,10 +8,7 @@ import { Node, GraphElement, RunStatus, - DEFAULT_LAYER, - Layer, ScaleDetailsLevel, - TOP_LAYER, NodeModel, useHover, PipelineNodeModel, @@ -33,7 +30,7 @@ type PipelinesDefaultGroupInnerProps = Omit = observer( ({ element, selected, onSelect }) => { - const [hover, hoverRef] = useHover(); + const [hover, hoverRef] = useHover(); const popoverRef = React.useRef(null); const detailsLevel = element.getGraph().getDetailsLevel(); @@ -65,7 +62,7 @@ const DefaultTaskGroupInner: React.FunctionComponent - }> - {element.isCollapsed() ? ( - - {groupNode} - - ) : ( - groupNode - )} - - + + {element.isCollapsed() ? ( + + {groupNode} + + ) : ( + groupNode + )} + ); }, ); diff --git a/frontend/src/concepts/topology/PipelineTaskEdge.tsx b/frontend/src/concepts/topology/PipelineTaskEdge.tsx index b81fb295a0..9cad5449a1 100644 --- a/frontend/src/concepts/topology/PipelineTaskEdge.tsx +++ b/frontend/src/concepts/topology/PipelineTaskEdge.tsx @@ -2,10 +2,10 @@ import * as React from 'react'; import { DEFAULT_SPACER_NODE_TYPE, GraphElement, - Edge, EdgeTerminalType, observer, TaskEdge, + isEdge, } from '@patternfly/react-topology'; interface PipelineTaskEdgeProps { @@ -13,7 +13,10 @@ interface PipelineTaskEdgeProps { } const PipelineTaskEdge: React.FC = ({ element, ...props }) => { - const edge = element as Edge; + if (!isEdge(element)) { + throw new Error('Element is not Edge'); + } + const edge = element; return ( = observer( ({ element, selected, onSelect, ...rest }) => { const bounds = element.getBounds(); - const [isHover, hoverRef] = useHover(); + const [isHover, hoverRef] = useHover(); const detailsLevel = element.getGraph().getDetailsLevel(); const data = element.getData(); const scale = element.getGraph().getScale(); @@ -120,10 +121,7 @@ const ArtifactTaskNodeInner: React.FC = observer( const translateX = bounds.width / 2 - (iconSize / 2) * upScale; const translateY = iconPadding * upScale; return ( - } - > + {isHover || detailsLevel !== ScaleDetailsLevel.high ? ( = ({ element, ...rest }) => ( - -); +const ArtifactTaskNode: React.FC = ({ element, ...rest }) => { + if (!isNode(element)) { + throw new Error('Element is not Node'); + } + return ; +}; export default ArtifactTaskNode; diff --git a/frontend/src/concepts/topology/customNodes/StandardTaskNode.tsx b/frontend/src/concepts/topology/customNodes/StandardTaskNode.tsx index 7bdca3198e..5c32a68623 100644 --- a/frontend/src/concepts/topology/customNodes/StandardTaskNode.tsx +++ b/frontend/src/concepts/topology/customNodes/StandardTaskNode.tsx @@ -28,7 +28,7 @@ const StandardTaskNode: React.FunctionComponent = ({ ...rest }) => { const data = element.getData(); - const [hover, hoverRef] = useHover(); + const [hover, hoverRef] = useHover(); const detailsLevel = element.getGraph().getDetailsLevel(); // Set the cached node status to Succeeded @@ -48,7 +48,7 @@ const StandardTaskNode: React.FunctionComponent = ({ ) : null; return ( - }> + ({ data: DEFAULT_CONTEXT_DATA, refreshState: async () => undefined, refreshAPIState: () => undefined, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions apiState: { apiAvailable: false, api: null as unknown as TrustyAPIState['api'] }, }); diff --git a/frontend/src/concepts/trustyai/useManageTrustyAICR.ts b/frontend/src/concepts/trustyai/useManageTrustyAICR.ts index f75e70e739..e4034d397d 100644 --- a/frontend/src/concepts/trustyai/useManageTrustyAICR.ts +++ b/frontend/src/concepts/trustyai/useManageTrustyAICR.ts @@ -26,20 +26,17 @@ const useManageTrustyAICR = (namespace: string): UseManageTrustyAICRReturnType = showSuccess.current = true; } - const installCR = React.useCallback( - () => - createTrustyAICR(namespace) - .then(refresh) - .catch((e) => setInstallReqError(e)), - [namespace, refresh], - ); + const installCR = React.useCallback(async () => { + await createTrustyAICR(namespace) + .then(refresh) + .catch((e) => setInstallReqError(e)); + }, [namespace, refresh]); - const deleteCR = React.useCallback( - () => deleteTrustyAICR(namespace).then(refresh), - [namespace, refresh], - ); + const deleteCR = React.useCallback(async () => { + await deleteTrustyAICR(namespace).then(refresh); + }, [namespace, refresh]); - return { + return { error, isProgressing, isAvailable, diff --git a/frontend/src/pages/BYONImages/BYONImageModal/AcceleratorIdentifierMultiselect.tsx b/frontend/src/pages/BYONImages/BYONImageModal/AcceleratorIdentifierMultiselect.tsx index a71eb628e4..5e646a6c60 100644 --- a/frontend/src/pages/BYONImages/BYONImageModal/AcceleratorIdentifierMultiselect.tsx +++ b/frontend/src/pages/BYONImages/BYONImageModal/AcceleratorIdentifierMultiselect.tsx @@ -170,7 +170,11 @@ export const AcceleratorIdentifierMultiselect: React.FC onSelect(selection as string)} + onSelect={(ev, selection) => { + if (typeof selection === 'string') { + onSelect(selection); + } + }} onOpenChange={() => setIsOpen(false)} toggle={toggle} > diff --git a/frontend/src/pages/exploreApplication/EnableModal.tsx b/frontend/src/pages/exploreApplication/EnableModal.tsx index 17cc480bb0..39016cdd5c 100644 --- a/frontend/src/pages/exploreApplication/EnableModal.tsx +++ b/frontend/src/pages/exploreApplication/EnableModal.tsx @@ -12,8 +12,8 @@ import { import { ExternalLinkAltIcon } from '@patternfly/react-icons'; import { OdhApplication } from '~/types'; import { EnableApplicationStatus, useEnableApplication } from '~/utilities/useEnableApplication'; +import { asEnumMember } from '~/utilities/utils'; import EnableVariable from './EnableVariable'; - import './EnableModal.scss'; type EnableModalProps = { @@ -158,7 +158,9 @@ const EnableModal: React.FC = ({ selectedApp, shown, onClose } key={key} ref={index === 0 ? focusRef : undefined} label={enable.variableDisplayText?.[key] ?? ''} - inputType={enable.variables?.[key] as TextInputTypes} + inputType={ + asEnumMember(enable.variables?.[key], TextInputTypes) ?? TextInputTypes.text + } helperText={enable.variableHelpText?.[key] ?? ''} validationInProgress={validationInProgress} value={enableValues[key]} diff --git a/frontend/src/pages/home/resources/useResourcesSection.tsx b/frontend/src/pages/home/resources/useResourcesSection.tsx index 4b78d05407..ab66e247a0 100644 --- a/frontend/src/pages/home/resources/useResourcesSection.tsx +++ b/frontend/src/pages/home/resources/useResourcesSection.tsx @@ -12,7 +12,6 @@ import { TextVariants, } from '@patternfly/react-core'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; -import { OdhDocument } from '~/types'; import OdhDocCard from '~/components/OdhDocCard'; import ScrolledGallery from '~/concepts/design/ScrolledGallery'; import CollapsibleSection from '~/concepts/design/CollapsibleSection'; @@ -65,7 +64,7 @@ export const useResourcesSection = (): React.ReactNode => { { + const foundDocs = specifiedDocs.reduce((acc, included) => { const doc = docs.find((d) => d.metadata.name === included.name && d.kind === included.kind); if (doc) { acc.push(doc); } return acc; - }, [] as OdhDocument[]); + }, []); return { docs: foundDocs, loaded, loadError }; }, [docs, specifiedDocs, loadError, loaded]); }; diff --git a/frontend/src/pages/learningCenter/ApplicationFilters.tsx b/frontend/src/pages/learningCenter/ApplicationFilters.tsx index eeb3b7f88f..389eec771b 100644 --- a/frontend/src/pages/learningCenter/ApplicationFilters.tsx +++ b/frontend/src/pages/learningCenter/ApplicationFilters.tsx @@ -34,8 +34,7 @@ const ApplicationFilters: React.FC = ({ docApps, catego return allApplications; }, [categoryApps, docApps]); - const onFilterChange = (docType: string, e: React.SyntheticEvent): void => { - const { checked } = e.target as React.AllHTMLAttributes; + const onFilterChange = (docType: string, checked: boolean): void => { const updatedQuery = [...providerFilters]; const index = updatedQuery.indexOf(docType); if (checked && index === -1) { @@ -71,7 +70,7 @@ const ApplicationFilters: React.FC = ({ docApps, catego id={application} key={application} checked={providerFilters.includes(application)} - onClick={(e) => onFilterChange(application, e)} + onChange={(_, checked) => onFilterChange(application, checked)} title={application} > {`${application} (${applications[application]})`} diff --git a/frontend/src/pages/learningCenter/DocTypeFilters.tsx b/frontend/src/pages/learningCenter/DocTypeFilters.tsx index 7b3d7c94ee..10ba55c421 100644 --- a/frontend/src/pages/learningCenter/DocTypeFilters.tsx +++ b/frontend/src/pages/learningCenter/DocTypeFilters.tsx @@ -4,6 +4,7 @@ import { FilterSidePanelCategory } from '@patternfly/react-catalog-view-extensio import FilterSidePanelCategoryItem from '~/components/FilterSidePanelCategoryItem'; import { OdhDocument, OdhDocumentType } from '~/types'; import { removeQueryArgument, setQueryArgument } from '~/utilities/router'; +import { asEnumMember, enumIterator } from '~/utilities/utils'; import { DOC_TYPE_FILTER_KEY } from './const'; import { useQueryFilters } from './useQueryFilters'; @@ -16,10 +17,11 @@ const DocTypeFilters: React.FC = ({ categoryApps }) => { const docTypeFilters = useQueryFilters(DOC_TYPE_FILTER_KEY); const docCounts = React.useMemo( () => - categoryApps.reduce( + categoryApps.reduce<{ [key in OdhDocumentType]: number }>( (acc, docApp) => { - if (docApp.spec.type in acc) { - acc[docApp.spec.type as keyof typeof acc]++; + const enumMember = asEnumMember(docApp.spec.type, OdhDocumentType); + if (enumMember) { + acc[enumMember]++; } return acc; }, @@ -52,26 +54,18 @@ const DocTypeFilters: React.FC = ({ categoryApps }) => { return ( - {Object.keys(OdhDocumentType).map((docType) => { - const value = OdhDocumentType[docType as keyof typeof OdhDocumentType]; - return ( - - onFilterChange( - value, - (e.target as React.AllHTMLAttributes).checked || false, - ) - } - title={docType} - > - {`${docType} (${docCounts[value]})`} - - ); - })} + {enumIterator(OdhDocumentType).map(([docType, value]) => ( + onFilterChange(value, checked)} + title={docType} + > + {`${docType} (${docCounts[value]})`} + + ))} ); }; diff --git a/frontend/src/pages/learningCenter/EnabledFilters.tsx b/frontend/src/pages/learningCenter/EnabledFilters.tsx index de72e83ea7..6c9b1799d2 100644 --- a/frontend/src/pages/learningCenter/EnabledFilters.tsx +++ b/frontend/src/pages/learningCenter/EnabledFilters.tsx @@ -47,12 +47,7 @@ const EnabledFilter: React.FC = ({ categoryApps }) => { data-id="enabled-filter-checkbox" id="enabled-filter-checkbox" checked={enabledFilters.includes('true')} - onClick={(e) => - onFilterChange( - 'true', - (e.target as React.AllHTMLAttributes).checked || false, - ) - } + onChange={(_, checked) => onFilterChange('true', checked)} title="Enabled" > {`Enabled (${enabledCount})`} @@ -61,12 +56,7 @@ const EnabledFilter: React.FC = ({ categoryApps }) => { data-id="not-enabled-filter-checkbox" id="not-enabled-filter-checkbox" checked={enabledFilters.includes('false')} - onClick={(e) => - onFilterChange( - 'false', - (e.target as React.AllHTMLAttributes).checked || false, - ) - } + onChange={(_, checked) => onFilterChange('false', checked)} title="Not enabled" > {`Not enabled (${notEnabledCount})`} diff --git a/frontend/src/pages/learningCenter/LearningCenterToolbar.tsx b/frontend/src/pages/learningCenter/LearningCenterToolbar.tsx index fda12ae799..bf10b8b391 100644 --- a/frontend/src/pages/learningCenter/LearningCenterToolbar.tsx +++ b/frontend/src/pages/learningCenter/LearningCenterToolbar.tsx @@ -113,7 +113,7 @@ const LearningCenterToolbar: React.FC = ({ const onSortTypeSelect = React.useCallback( (e: React.MouseEvent | React.ChangeEvent) => { setIsSortTypeDropdownOpen(false); - const selection = (e.target as Element).getAttribute('data-key') ?? ''; + const selection = e.target instanceof Element ? e.target.getAttribute('data-key') ?? '' : ''; setQueryArgument(navigate, DOC_SORT_KEY, selection); }, [navigate], @@ -133,7 +133,7 @@ const LearningCenterToolbar: React.FC = ({ const onSortOrderSelect = React.useCallback( (e: React.MouseEvent | React.ChangeEvent) => { setIsSortOrderDropdownOpen(false); - const selection = (e.target as Element).getAttribute('data-key') ?? ''; + const selection = e.target instanceof Element ? e.target.getAttribute('data-key') ?? '' : ''; setQueryArgument(navigate, DOC_SORT_ORDER_KEY, selection); }, [navigate], diff --git a/frontend/src/pages/learningCenter/ProviderTypeFilters.tsx b/frontend/src/pages/learningCenter/ProviderTypeFilters.tsx index ffb216ca36..21eb0e1538 100644 --- a/frontend/src/pages/learningCenter/ProviderTypeFilters.tsx +++ b/frontend/src/pages/learningCenter/ProviderTypeFilters.tsx @@ -39,8 +39,7 @@ const ProviderTypeFilters: React.FC = ({ docApps, cate return allTypes; }, [categoryApps, docApps]); - const onFilterChange = (docType: string, e: React.SyntheticEvent): void => { - const { checked } = e.target as React.AllHTMLAttributes; + const onFilterChange = (docType: string, checked: boolean): void => { const updatedQuery = [...providerTypeFilters]; const index = updatedQuery.indexOf(docType); if (checked && index === -1) { @@ -81,7 +80,7 @@ const ProviderTypeFilters: React.FC = ({ docApps, cate id={providerType} key={providerType} checked={providerTypeFilters.includes(providerType)} - onClick={(e) => onFilterChange(providerType, e)} + onChange={(_, checked) => onFilterChange(providerType, checked)} title={providerType} > {`${providerType} (${providerTypes[providerType]})`} diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionSelector.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionSelector.tsx index cf00d689ea..80aadb7cca 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionSelector.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionSelector.tsx @@ -53,8 +53,10 @@ const ModelVersionSelector: React.FC = ({ const menu = ( { - onSelect(itemId as string); - setOpen(false); + if (typeof itemId === 'string') { + onSelect(itemId); + setOpen(false); + } }} data-id="model-version-selector-menu" ref={menuRef} diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionListView.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionListView.tsx index 049454ed25..2bad667c01 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionListView.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionListView.tsx @@ -22,6 +22,7 @@ import EmptyModelRegistryState from '~/pages/modelRegistry/screens/components/Em import { filterModelVersions } from '~/pages/modelRegistry/screens/utils'; import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext'; import { modelVersionArchiveUrl } from '~/pages/modelRegistry/screens/routeUtils'; +import { asEnumMember } from '~/utilities/utils'; import ModelVersionsTable from './ModelVersionsTable'; type ModelVersionListViewProps = { @@ -89,7 +90,10 @@ const ModelVersionListView: React.FC = ({ }))} value={searchType} onChange={(newSearchType) => { - setSearchType(newSearchType as SearchType); + const enumMember = asEnumMember(newSearchType, SearchType); + if (enumMember !== null) { + setSearchType(enumMember); + } }} icon={} /> diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionsArchiveListView.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionsArchiveListView.tsx index e7bf2dcc50..800c4a3e21 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionsArchiveListView.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionsArchiveListView.tsx @@ -13,6 +13,7 @@ import { ModelVersion } from '~/concepts/modelRegistry/types'; import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; import { filterModelVersions } from '~/pages/modelRegistry/screens/utils'; import EmptyModelRegistryState from '~/pages/modelRegistry/screens/components/EmptyModelRegistryState'; +import { asEnumMember } from '~/utilities/utils'; import ModelVersionsArchiveTable from './ModelVersionsArchiveTable'; type ModelVersionsArchiveListViewProps = { @@ -63,7 +64,10 @@ const ModelVersionsArchiveListView: React.FC }))} value={searchType} onChange={(newSearchType) => { - setSearchType(newSearchType as SearchType); + const enumMember = asEnumMember(newSearchType, SearchType); + if (enumMember) { + setSearchType(enumMember); + } }} icon={} /> diff --git a/frontend/src/pages/modelRegistry/screens/RegisteredModels/RegisteredModelListView.tsx b/frontend/src/pages/modelRegistry/screens/RegisteredModels/RegisteredModelListView.tsx index 561cee5da8..3ad9e8be51 100644 --- a/frontend/src/pages/modelRegistry/screens/RegisteredModels/RegisteredModelListView.tsx +++ b/frontend/src/pages/modelRegistry/screens/RegisteredModels/RegisteredModelListView.tsx @@ -9,6 +9,7 @@ import { filterRegisteredModels } from '~/pages/modelRegistry/screens/utils'; import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext'; import EmptyModelRegistryState from '~/pages/modelRegistry/screens/components/EmptyModelRegistryState'; import { registeredModelArchiveUrl } from '~/pages/modelRegistry/screens/routeUtils'; +import { asEnumMember } from '~/utilities/utils'; import RegisteredModelTable from './RegisteredModelTable'; import RegisteredModelsTableToolbar from './RegisteredModelsTableToolbar'; @@ -71,7 +72,10 @@ const RegisteredModelListView: React.FC = ({ }))} value={searchType} onChange={(newSearchType) => { - setSearchType(newSearchType as SearchType); + const newSearchTypeInput = asEnumMember(newSearchType, SearchType); + if (newSearchTypeInput !== null) { + setSearchType(newSearchTypeInput); + } }} icon={} /> diff --git a/frontend/src/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelsArchiveListView.tsx b/frontend/src/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelsArchiveListView.tsx index 7e5de0392c..86a0f2e9db 100644 --- a/frontend/src/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelsArchiveListView.tsx +++ b/frontend/src/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelsArchiveListView.tsx @@ -13,6 +13,7 @@ import { RegisteredModel } from '~/concepts/modelRegistry/types'; import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; import { filterRegisteredModels } from '~/pages/modelRegistry/screens/utils'; import EmptyModelRegistryState from '~/pages/modelRegistry/screens/components/EmptyModelRegistryState'; +import { asEnumMember } from '~/utilities/utils'; import RegisteredModelsArchiveTable from './RegisteredModelsArchiveTable'; type RegisteredModelsArchiveListViewProps = { @@ -68,7 +69,10 @@ const RegisteredModelsArchiveListView: React.FC { - setSearchType(newSearchType as SearchType); + const newSearchTypeInput = asEnumMember(newSearchType, SearchType); + if (newSearchTypeInput !== null) { + setSearchType(newSearchTypeInput); + } }} icon={} /> diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeAPIProtocolSelector.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeAPIProtocolSelector.tsx index 2b9d485279..b22321b07e 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeAPIProtocolSelector.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeAPIProtocolSelector.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { FormGroup } from '@patternfly/react-core'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; +import { asEnumMember } from '~/utilities/utils'; type CustomServingRuntimeAPIProtocolSelectorProps = { selectedAPIProtocol: ServingRuntimeAPIProtocol | undefined; @@ -50,7 +51,12 @@ const CustomServingRuntimeAPIProtocolSelector: React.FC< isDisabled={isOnlyModelMesh} options={options} value={selectedAPIProtocol || ''} - onChange={(key) => setSelectedAPIProtocol(key as ServingRuntimeAPIProtocol)} + onChange={(key) => { + const enumValue = asEnumMember(key, ServingRuntimeAPIProtocol); + if (enumValue !== null) { + setSelectedAPIProtocol(enumValue); + } + }} /> ); diff --git a/frontend/src/pages/modelServing/customServingRuntimes/utils.ts b/frontend/src/pages/modelServing/customServingRuntimes/utils.ts index 21330644fc..6e0c34f66c 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/utils.ts +++ b/frontend/src/pages/modelServing/customServingRuntimes/utils.ts @@ -2,6 +2,7 @@ import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils'; import { ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; +import { asEnumMember } from '~/utilities/utils'; export const getTemplateEnabled = ( template: TemplateKind, @@ -142,8 +143,12 @@ export const getAPIProtocolFromTemplate = ( if (!template.metadata.annotations?.['opendatahub.io/apiProtocol']) { return undefined; } - - return template.metadata.annotations['opendatahub.io/apiProtocol'] as ServingRuntimeAPIProtocol; + return ( + asEnumMember( + template.metadata.annotations['opendatahub.io/apiProtocol'], + ServingRuntimeAPIProtocol, + ) ?? undefined + ); }; export const getAPIProtocolFromServingRuntime = ( @@ -152,5 +157,10 @@ export const getAPIProtocolFromServingRuntime = ( if (!resource.metadata.annotations?.['opendatahub.io/apiProtocol']) { return undefined; } - return resource.metadata.annotations['opendatahub.io/apiProtocol'] as ServingRuntimeAPIProtocol; + return ( + asEnumMember( + resource.metadata.annotations['opendatahub.io/apiProtocol'], + ServingRuntimeAPIProtocol, + ) ?? undefined + ); }; diff --git a/frontend/src/pages/modelServing/screens/global/utils.ts b/frontend/src/pages/modelServing/screens/global/utils.ts index b2728f34a6..d4d4c22111 100644 --- a/frontend/src/pages/modelServing/screens/global/utils.ts +++ b/frontend/src/pages/modelServing/screens/global/utils.ts @@ -2,6 +2,7 @@ import { InferenceServiceKind, ProjectKind, SecretKind, PodKind } from '~/k8sTyp import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; import { getProjectDisplayName } from '~/concepts/projects/utils'; import { InferenceServiceModelState, ModelStatus } from '~/pages/modelServing/screens/types'; +import { asEnumMember } from '~/utilities/utils'; export const getInferenceServiceDisplayName = (is: InferenceServiceKind): string => getDisplayNameFromK8sResource(is); @@ -12,8 +13,8 @@ export const getTokenDisplayName = (secret: SecretKind): string => export const getInferenceServiceActiveModelState = ( is: InferenceServiceKind, ): InferenceServiceModelState => - is.status?.modelStatus?.states?.activeModelState || - is.status?.modelStatus?.states?.targetModelState || + asEnumMember(is.status?.modelStatus?.states?.activeModelState, InferenceServiceModelState) || + asEnumMember(is.status?.modelStatus?.states?.targetModelState, InferenceServiceModelState) || InferenceServiceModelState.UNKNOWN; export const getInferenceServiceStatusMessage = (is: InferenceServiceKind): string => { diff --git a/frontend/src/pages/modelServing/screens/metrics/EnsureMetricsAvailable.tsx b/frontend/src/pages/modelServing/screens/metrics/EnsureMetricsAvailable.tsx index 85c1684d85..82a4fd840d 100644 --- a/frontend/src/pages/modelServing/screens/metrics/EnsureMetricsAvailable.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/EnsureMetricsAvailable.tsx @@ -28,8 +28,9 @@ const EnsureMetricsAvailable: React.FC = ({ let readyCount = 0; metrics.forEach((metric) => { - if (data[metric].error) { - error = data[metric].error as AxiosError; + const axiosError = data[metric].error; + if (axiosError && axiosError instanceof AxiosError) { + error = axiosError; } if (data[metric].loaded) { readyCount++; diff --git a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx b/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx index d7981dad08..e757df110e 100644 --- a/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/ModelServingMetricsContext.tsx @@ -24,10 +24,16 @@ export enum ModelMetricType { } type ModelServingMetricsContextType = { - data: Record< - ModelMetricType | ServerMetricType, - ContextResourceData - >; + data: { + [ServerMetricType.REQUEST_COUNT]: ContextResourceData; + [ServerMetricType.AVG_RESPONSE_TIME]: ContextResourceData; + [ServerMetricType.CPU_UTILIZATION]: ContextResourceData; + [ServerMetricType.MEMORY_UTILIZATION]: ContextResourceData; + [ModelMetricType.REQUEST_COUNT_FAILED]: ContextResourceData; + [ModelMetricType.REQUEST_COUNT_SUCCESS]: ContextResourceData; + [ModelMetricType.TRUSTY_AI_SPD]: ContextResourceData; + [ModelMetricType.TRUSTY_AI_DIR]: ContextResourceData; + }; refresh: () => void; namespace: string; }; diff --git a/frontend/src/pages/modelServing/screens/metrics/bias/BiasChart.tsx b/frontend/src/pages/modelServing/screens/metrics/bias/BiasChart.tsx index 89d7018bfe..ade194fdc7 100644 --- a/frontend/src/pages/modelServing/screens/metrics/bias/BiasChart.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/bias/BiasChart.tsx @@ -19,7 +19,9 @@ const BiasChart: React.FC = ({ biasMetricConfig }) => { BIAS_CHART_CONFIGS[metricType]; const metric = React.useMemo(() => { - const metricData = data[modelMetricKey].data as PrometheusQueryRangeResponseDataResult[]; + const metricData = data[modelMetricKey].data.filter( + (x): x is PrometheusQueryRangeResponseDataResult => 'metric' in x, + ); const values = metricData.find((x) => x.metric.request === id)?.values || []; diff --git a/frontend/src/pages/modelServing/screens/metrics/bias/BiasConfigurationPage/BiasConfigurationModal/MetricTypeField.tsx b/frontend/src/pages/modelServing/screens/metrics/bias/BiasConfigurationPage/BiasConfigurationModal/MetricTypeField.tsx index fd7c60a5f1..e50085df55 100644 --- a/frontend/src/pages/modelServing/screens/metrics/bias/BiasConfigurationPage/BiasConfigurationModal/MetricTypeField.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/bias/BiasConfigurationPage/BiasConfigurationModal/MetricTypeField.tsx @@ -7,6 +7,7 @@ import { } from '~/pages/modelServing/screens/metrics/const'; import { BiasMetricType } from '~/api'; import { isMetricType } from '~/pages/modelServing/screens/metrics/utils'; +import { enumIterator } from '~/utilities/utils'; type MetricTypeFieldProps = { fieldId: string; @@ -16,6 +17,7 @@ type MetricTypeFieldProps = { const MetricTypeField: React.FC = ({ fieldId, value, onChange }) => { const [isOpen, setOpen] = React.useState(false); + return ( diff --git a/frontend/src/pages/modelServing/screens/metrics/performance/ModelMeshMetrics.tsx b/frontend/src/pages/modelServing/screens/metrics/performance/ModelMeshMetrics.tsx index 8f9c511223..ed008ecf21 100644 --- a/frontend/src/pages/modelServing/screens/metrics/performance/ModelMeshMetrics.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/performance/ModelMeshMetrics.tsx @@ -5,7 +5,6 @@ import { ModelMetricType, ModelServingMetricsContext, } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; -import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; import EnsureMetricsAvailable from '~/pages/modelServing/screens/metrics/EnsureMetricsAvailable'; const ModelMeshMetrics: React.FC = () => { @@ -22,15 +21,11 @@ const ModelMeshMetrics: React.FC = () => { metrics={[ { name: 'Successful', - metric: data[ - ModelMetricType.REQUEST_COUNT_SUCCESS - ] as ContextResourceData, + metric: data[ModelMetricType.REQUEST_COUNT_SUCCESS], }, { name: 'Failed', - metric: data[ - ModelMetricType.REQUEST_COUNT_FAILED - ] as ContextResourceData, + metric: data[ModelMetricType.REQUEST_COUNT_FAILED], }, ]} color="blue" diff --git a/frontend/src/pages/modelServing/screens/metrics/performance/ServerGraphs.tsx b/frontend/src/pages/modelServing/screens/metrics/performance/ServerGraphs.tsx index bcf76a0b15..d97fc00a2a 100644 --- a/frontend/src/pages/modelServing/screens/metrics/performance/ServerGraphs.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/performance/ServerGraphs.tsx @@ -9,11 +9,6 @@ import { convertPrometheusNaNToZero, toPercentage, } from '~/pages/modelServing/screens/metrics/utils'; -import { - ContextResourceData, - PrometheusQueryRangeResponseDataResult, - PrometheusQueryRangeResultValue, -} from '~/types'; import { NamedMetricChartLine } from '~/pages/modelServing/screens/metrics/types'; const ServerGraphs: React.FC = () => { @@ -24,9 +19,7 @@ const ServerGraphs: React.FC = () => { , + metric: data[ServerMetricType.REQUEST_COUNT], }} color="blue" title="HTTP requests per 5 minutes" @@ -35,11 +28,7 @@ const ServerGraphs: React.FC = () => { - ).data.map( + metrics={data[ServerMetricType.AVG_RESPONSE_TIME].data.map( (line): NamedMetricChartLine => ({ name: line.metric.pod || '', metric: { @@ -56,9 +45,7 @@ const ServerGraphs: React.FC = () => { , + metric: data[ServerMetricType.CPU_UTILIZATION], translatePoint: toPercentage, }} color="purple" @@ -71,9 +58,7 @@ const ServerGraphs: React.FC = () => { , + metric: data[ServerMetricType.MEMORY_UTILIZATION], translatePoint: toPercentage, }} color="orange" diff --git a/frontend/src/pages/modelServing/screens/metrics/utils.tsx b/frontend/src/pages/modelServing/screens/metrics/utils.tsx index f82f06e592..2f878d30cd 100644 --- a/frontend/src/pages/modelServing/screens/metrics/utils.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/utils.tsx @@ -14,6 +14,7 @@ import { BIAS_THRESHOLD_COLOR, } from '~/pages/modelServing/screens/metrics/const'; import { PROMETHEUS_REQUEST_RESOLUTION } from '~/concepts/metrics/const'; +import { isEnumMember } from '~/utilities/utils'; import { BiasSelectOption, DomainCalculator, @@ -219,8 +220,7 @@ export const checkConfigurationFieldsValid = ( export const isMetricType = ( metricType: string | SelectOptionObject, -): metricType is BiasMetricType => - Object.values(BiasMetricType).includes(metricType as BiasMetricType); +): metricType is BiasMetricType => isEnumMember(metricType.toString(), BiasMetricType); export const byId = (arg: U): ((arg: T) => boolean) => diff --git a/frontend/src/pages/notebookController/screens/admin/NotebookControllerTabs.tsx b/frontend/src/pages/notebookController/screens/admin/NotebookControllerTabs.tsx index e85b2193fb..0a76b36591 100644 --- a/frontend/src/pages/notebookController/screens/admin/NotebookControllerTabs.tsx +++ b/frontend/src/pages/notebookController/screens/admin/NotebookControllerTabs.tsx @@ -3,6 +3,7 @@ import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; import { NotebookControllerTabTypes } from '~/pages/notebookController/const'; import NotebookServerRoutes from '~/pages/notebookController/screens/server/NotebookServerRoutes'; import { NotebookControllerContext } from '~/pages/notebookController/NotebookControllerContext'; +import { asEnumMember } from '~/utilities/utils'; import NotebookAdmin from './NotebookAdmin'; const NotebookControllerTabs: React.FC = () => { @@ -16,7 +17,10 @@ const NotebookControllerTabs: React.FC = () => { unmountOnExit onSelect={(e, eventKey) => { setImpersonating(); - setCurrentAdminTab(eventKey as NotebookControllerTabTypes); + const enumValue = asEnumMember(eventKey, NotebookControllerTabTypes); + if (enumValue !== null) { + setCurrentAdminTab(enumValue); + } }} > = ({ user, userProperty }) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const content = user[userProperty as keyof AdminViewUserData]; if (isField(content, userProperty === 'serverStatus')) { diff --git a/frontend/src/pages/notebookController/screens/server/EnvironmentVariablesField.tsx b/frontend/src/pages/notebookController/screens/server/EnvironmentVariablesField.tsx index 7234cad59f..37fc1ab4e2 100644 --- a/frontend/src/pages/notebookController/screens/server/EnvironmentVariablesField.tsx +++ b/frontend/src/pages/notebookController/screens/server/EnvironmentVariablesField.tsx @@ -17,6 +17,7 @@ import { CUSTOM_VARIABLE, EMPTY_KEY } from '~/pages/notebookController/const'; import { EnvVarType, VariableRow } from '~/types'; import '~/pages/notebookController/NotebookController.scss'; +import { asEnumMember } from '~/utilities/utils'; type EnvironmentVariablesFieldProps = { fieldIndex: string; @@ -88,7 +89,7 @@ const EnvironmentVariablesField: React.FC = ({ type={ showPassword && variableType === 'password' ? TextInputTypes.text - : (variable.type as TextInputTypes) + : asEnumMember(variable.type, TextInputTypes) ?? undefined } value={variable.value} onChange={(e, newValue) => diff --git a/frontend/src/pages/notebookController/screens/server/EnvironmentVariablesRow.tsx b/frontend/src/pages/notebookController/screens/server/EnvironmentVariablesRow.tsx index 84a99a7aca..4b3192712e 100644 --- a/frontend/src/pages/notebookController/screens/server/EnvironmentVariablesRow.tsx +++ b/frontend/src/pages/notebookController/screens/server/EnvironmentVariablesRow.tsx @@ -82,7 +82,9 @@ const EnvironmentVariablesRow: React.FC = ({ onToggle={() => setTypeDropdownOpen(!typeDropdownOpen)} aria-labelledby="container-size" selections={variableRow.variableType} - onSelect={(e, selection) => updateVariableType(selection as string)} + onSelect={(e, selection) => { + updateVariableType(selection.toString()); + }} > {selectOptions} diff --git a/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx b/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx index 1b92deb875..720f13bb32 100644 --- a/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx +++ b/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx @@ -26,7 +26,7 @@ const GlobalPipelineCoreDetails: React.FC = ({ [ ( @@ -56,7 +56,7 @@ export const GlobalExperimentDetails: React.FC< [ Experiments - {getProjectDisplayName(project)} @@ -64,7 +64,7 @@ export const GlobalExperimentDetails: React.FC< , {experiment?.display_name ? ( - + {experiment.display_name} ) : ( diff --git a/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx b/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx index 2e4df9f1bf..1b99b7143d 100644 --- a/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx +++ b/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx @@ -9,6 +9,8 @@ import { ExperimentRunsContext } from '~/pages/pipelines/global/experiments/Expe export const ExperimentRunsListBreadcrumb: React.FC = () => { const { experiment } = React.useContext(ExperimentRunsContext); + const displayName = experiment?.display_name || 'Loading...'; + return ( @@ -16,9 +18,13 @@ export const ExperimentRunsListBreadcrumb: React.FC = () => { - - {experiment?.storage_state === StorageStateKF.ARCHIVED && } + {/* A hack solution to get rid of the minWidth set on PF Truncate component + So we can show correct spacing between the title and the label + The min width is set to 12 characters: + https://github.com/patternfly/patternfly/blob/9499f0a70a18f51474285752a04928958d901829/src/patternfly/components/Truncate/truncate.scss#L4 */} + {displayName.length > 12 ? : <>{displayName}} + {experiment?.storage_state === StorageStateKF.ARCHIVED && } ); }; diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx index 1ae310c802..f43a652aab 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx @@ -61,7 +61,7 @@ export const ArtifactDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPa loadError={artifactError} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath()} diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx index 4cc242a507..aa6f49ac22 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx @@ -34,7 +34,7 @@ export const ArtifactOverviewDetails: React.FC = ( <> URI - + )} diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx index 4285892466..49ca53034f 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx @@ -151,7 +151,7 @@ export const ArtifactsTable: React.FC = ({ {artifact.id} {artifact.type} - + diff --git a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx index eaa594cfd8..5f48db21af 100644 --- a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx +++ b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx @@ -21,7 +21,7 @@ const CompareRunsPage: React.FC = ({ breadcrumbPath }) => { title="" breadcrumb={ - {breadcrumbPath} + {breadcrumbPath()} Compare runs } diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx index 1090db9ce6..63a3c4533b 100644 --- a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx @@ -44,10 +44,10 @@ const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, co const [artifactTypes, artifactTypesLoaded] = useGetArtifactTypes(); const allEvents = parseEventsByType(events); - const artifactTypeMap = artifactTypes.reduce((acc, artifactType) => { + const artifactTypeMap = artifactTypes.reduce>((acc, artifactType) => { acc[artifactType.getId()] = artifactType.getName(); return acc; - }, {} as Record); + }, {}); const error = executionError || eventsError; @@ -96,7 +96,7 @@ const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, co loaded breadcrumb={ - {breadcrumbPath} + {breadcrumbPath()} {displayName} } diff --git a/frontend/src/pages/pipelines/global/runs/GlobalPipelineRunsTabs.tsx b/frontend/src/pages/pipelines/global/runs/GlobalPipelineRunsTabs.tsx index a2e2e6a45e..62ddbacdfd 100644 --- a/frontend/src/pages/pipelines/global/runs/GlobalPipelineRunsTabs.tsx +++ b/frontend/src/pages/pipelines/global/runs/GlobalPipelineRunsTabs.tsx @@ -30,7 +30,12 @@ const GlobalPipelineRunsTab: React.FC = () => { return ( setSearchParams({ runType: tabId as PipelineRunType })} + onSelect={(_event, tabId) => { + const enumValue = asEnumMember(tabId, PipelineRunType); + if (enumValue !== null) { + setSearchParams({ runType: enumValue }); + } + }} aria-label="Pipeline run page tabs" role="region" className="odh-pipeline-runs-page-tabs" diff --git a/frontend/src/pages/pipelines/global/runs/GlobalPipelineVersionRuns.tsx b/frontend/src/pages/pipelines/global/runs/GlobalPipelineVersionRuns.tsx index c6c634b163..7a756cd3fc 100644 --- a/frontend/src/pages/pipelines/global/runs/GlobalPipelineVersionRuns.tsx +++ b/frontend/src/pages/pipelines/global/runs/GlobalPipelineVersionRuns.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Link, useParams } from 'react-router-dom'; -import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; +import { Breadcrumb, BreadcrumbItem, Truncate } from '@patternfly/react-core'; import ApplicationsPage from '~/pages/ApplicationsPage'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { PipelineCoreDetailsPageComponent } from '~/concepts/pipelines/content/types'; @@ -29,7 +29,7 @@ const GlobalPipelineVersionRuns: PipelineCoreDetailsPageComponent = ({ breadcrum - {breadcrumbPath} + {breadcrumbPath()} {title} } @@ -46,10 +46,14 @@ const GlobalPipelineVersionRuns: PipelineCoreDetailsPageComponent = ({ breadcrum - {breadcrumbPath} + {breadcrumbPath()} - {pipelineVersion?.display_name || 'Loading...'} + {/* TODO: Remove the custom className after upgrading to PFv6 */} + Runs diff --git a/frontend/src/pages/projects/ProjectDetailsContext.tsx b/frontend/src/pages/projects/ProjectDetailsContext.tsx index 825fb866ec..2e7ea07e1d 100644 --- a/frontend/src/pages/projects/ProjectDetailsContext.tsx +++ b/frontend/src/pages/projects/ProjectDetailsContext.tsx @@ -52,6 +52,7 @@ type ProjectDetailsContextType = { export const ProjectDetailsContext = React.createContext({ // We never will get into a case without a project, so fudge the default value + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions currentProject: null as unknown as ProjectKind, refreshAllProjectData: () => undefined, filterTokens: () => [], diff --git a/frontend/src/pages/projects/screens/detail/overview/serverModels/deployedModels/DeployedModelsSection.tsx b/frontend/src/pages/projects/screens/detail/overview/serverModels/deployedModels/DeployedModelsSection.tsx index 6a2b420559..0561d4d81e 100644 --- a/frontend/src/pages/projects/screens/detail/overview/serverModels/deployedModels/DeployedModelsSection.tsx +++ b/frontend/src/pages/projects/screens/detail/overview/serverModels/deployedModels/DeployedModelsSection.tsx @@ -58,11 +58,14 @@ const DeployedModelsSection: React.FC = ({ isMultiPl return; } if (isMultiPlatform) { - const modelInferenceServices = modelServers.reduce((acc, modelServer) => { - const services = getInferenceServiceFromServingRuntime(inferenceServices, modelServer); - acc.push(...services); - return acc; - }, [] as InferenceServiceKind[]); + const modelInferenceServices = modelServers.reduce( + (acc, modelServer) => { + const services = getInferenceServiceFromServingRuntime(inferenceServices, modelServer); + acc.push(...services); + return acc; + }, + [], + ); setDeployedModels(modelInferenceServices); } else { setDeployedModels(inferenceServices); diff --git a/frontend/src/pages/projects/screens/detail/pipelines/ProjectPipelineBreadcrumbPage.tsx b/frontend/src/pages/projects/screens/detail/pipelines/ProjectPipelineBreadcrumbPage.tsx index 37c751d9b6..da3400b369 100644 --- a/frontend/src/pages/projects/screens/detail/pipelines/ProjectPipelineBreadcrumbPage.tsx +++ b/frontend/src/pages/projects/screens/detail/pipelines/ProjectPipelineBreadcrumbPage.tsx @@ -19,7 +19,7 @@ const ProjectPipelineBreadcrumbPage: React.FC = ({ return ( [ Data Science Projects} diff --git a/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvConfigMap.tsx b/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvConfigMap.tsx index 087af37f9d..7d876cb12b 100644 --- a/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvConfigMap.tsx +++ b/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvConfigMap.tsx @@ -4,6 +4,7 @@ import { EnvironmentVariableType, EnvVariableData, } from '~/pages/projects/types'; +import { asEnumMember } from '~/utilities/utils'; import EnvDataTypeField from './EnvDataTypeField'; import GenericKeyValuePairField from './GenericKeyValuePairField'; import { EMPTY_KEY_VALUE_PAIR } from './const'; @@ -22,7 +23,9 @@ const DEFAULT_ENV: EnvVariableData = { const EnvConfigMap: React.FC = ({ env = DEFAULT_ENV, onUpdate }) => ( onUpdate({ ...env, category: value as ConfigMapCategory, data: [] })} + onSelection={(value) => + onUpdate({ ...env, category: asEnumMember(value, ConfigMapCategory), data: [] }) + } options={{ [ConfigMapCategory.GENERIC]: { label: 'Key / value', diff --git a/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvSecret.tsx b/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvSecret.tsx index b5abf71d37..aab8fe07c0 100644 --- a/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvSecret.tsx +++ b/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvSecret.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { EnvironmentVariableType, EnvVariableData, SecretCategory } from '~/pages/projects/types'; +import { asEnumMember } from '~/utilities/utils'; import EnvDataTypeField from './EnvDataTypeField'; import GenericKeyValuePairField from './GenericKeyValuePairField'; import { EMPTY_KEY_VALUE_PAIR } from './const'; @@ -18,7 +19,9 @@ const DEFAULT_ENV: EnvVariableData = { const EnvSecret: React.FC = ({ env = DEFAULT_ENV, onUpdate }) => ( onUpdate({ ...env, category: value as SecretCategory, data: [] })} + onSelection={(value) => + onUpdate({ ...env, category: asEnumMember(value, SecretCategory), data: [] }) + } options={{ [SecretCategory.GENERIC]: { label: 'Key / value', diff --git a/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvTypeSelectField.tsx b/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvTypeSelectField.tsx index 259f796cb3..a882c76690 100644 --- a/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvTypeSelectField.tsx +++ b/frontend/src/pages/projects/screens/spawner/environmentVariables/EnvTypeSelectField.tsx @@ -4,6 +4,7 @@ import { Select, SelectOption } from '@patternfly/react-core/deprecated'; import { MinusCircleIcon } from '@patternfly/react-icons'; import { EnvironmentVariableType, EnvVariable } from '~/pages/projects/types'; import IndentSection from '~/pages/projects/components/IndentSection'; +import { asEnumMember } from '~/utilities/utils'; import EnvTypeSwitch from './EnvTypeSwitch'; type EnvTypeSelectFieldProps = { @@ -32,10 +33,13 @@ const EnvTypeSelectField: React.FC = ({ aria-label="Select environment variable type" onSelect={(e, value) => { if (typeof value === 'string') { - onUpdate({ - type: value as EnvironmentVariableType, - }); - setOpen(false); + const enumValue = asEnumMember(value, EnvironmentVariableType); + if (enumValue !== null) { + onUpdate({ + type: enumValue, + }); + setOpen(false); + } } }} > diff --git a/frontend/src/pages/projects/screens/spawner/environmentVariables/utils.ts b/frontend/src/pages/projects/screens/spawner/environmentVariables/utils.ts index d49b49aae5..e73f077feb 100644 --- a/frontend/src/pages/projects/screens/spawner/environmentVariables/utils.ts +++ b/frontend/src/pages/projects/screens/spawner/environmentVariables/utils.ts @@ -7,12 +7,14 @@ export const removeArrayItem = (values: T[], index: number): T[] => values.filter((v, i) => i !== index); export const isConfigMapKind = (object: unknown): object is ConfigMapKind => - (object as ConfigMapKind).kind === 'ConfigMap'; + typeof object === 'object' && object !== null && 'kind' in object && object.kind === 'ConfigMap'; export const isSecretKind = (object: unknown): object is SecretKind => - (object as SecretKind).kind === 'Secret'; + typeof object === 'object' && object !== null && 'kind' in object && object.kind === 'Secret'; export const isStringKeyValuePairObject = (object: unknown): object is Record => - Object.entries(object as Record).every( + typeof object === 'object' && + object !== null && + Object.entries(object).every( ([key, value]) => typeof key === 'string' && typeof value === 'string', ); diff --git a/frontend/src/pages/projects/screens/spawner/spawnerUtils.ts b/frontend/src/pages/projects/screens/spawner/spawnerUtils.ts index 54bbf8397f..b68507988d 100644 --- a/frontend/src/pages/projects/screens/spawner/spawnerUtils.ts +++ b/frontend/src/pages/projects/screens/spawner/spawnerUtils.ts @@ -81,7 +81,10 @@ export const getImageVersionSelectOptionObject = ( export const isImageVersionSelectOptionObject = ( object: unknown, ): object is ImageVersionSelectOptionObjectType => - (object as ImageVersionSelectOptionObjectType | undefined)?.imageVersion !== undefined; + typeof object === 'object' && + object !== null && + 'imageVersion' in object && + object.imageVersion !== undefined; /******************* Compare utils for sorting *******************/ const getBuildNumber = (build: BuildKind): number => { const buildNumber = build.metadata.annotations?.['openshift.io/build.number'] || '-1'; diff --git a/frontend/src/pages/projects/screens/spawner/useBuildStatuses.ts b/frontend/src/pages/projects/screens/spawner/useBuildStatuses.ts index 4c2c3987f3..0e08b4e05f 100644 --- a/frontend/src/pages/projects/screens/spawner/useBuildStatuses.ts +++ b/frontend/src/pages/projects/screens/spawner/useBuildStatuses.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { getNotebookBuildConfigs, getBuildsForBuildConfig } from '~/api'; import useNotification from '~/utilities/useNotification'; -import { BuildConfigKind, BuildKind, BuildPhase } from '~/k8sTypes'; +import { BuildConfigKind, BuildPhase } from '~/k8sTypes'; import { BuildStatus } from './types'; import { compareBuilds } from './spawnerUtils'; @@ -26,7 +26,7 @@ const useBuildStatuses = (namespace?: string): BuildStatus[] => { imageStreamVersion: buildConfig.spec.output.to.name, }; } - const mostRecent = builds.toSorted(compareBuilds).pop() as BuildKind; + const mostRecent = builds.toSorted(compareBuilds)[builds.length - 1]; return { name: buildNotebookName, status: mostRecent.status.phase, diff --git a/frontend/src/redux/context.ts b/frontend/src/redux/context.ts index 82105b8f35..e93bf2751a 100644 --- a/frontend/src/redux/context.ts +++ b/frontend/src/redux/context.ts @@ -2,5 +2,6 @@ import * as React from 'react'; import { ReactReduxContextValue } from 'react-redux'; export const ReduxContext = React.createContext( + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions {} as ReactReduxContextValue, ); diff --git a/frontend/src/redux/store/store.ts b/frontend/src/redux/store/store.ts index 5eb7522efd..aa01b385ce 100644 --- a/frontend/src/redux/store/store.ts +++ b/frontend/src/redux/store/store.ts @@ -5,7 +5,7 @@ import appReducer from '~/redux/reducers/appReducer'; import { ODH_PRODUCT_NAME } from '~/utilities/const'; const composeEnhancers = - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?.({ name: ODH_PRODUCT_NAME, }) || compose; @@ -16,7 +16,7 @@ export const store = configureStore(); // Create a separate for the for the dynamic plugin SDK const sdkComposeEnhancers = - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?.({ name: `${ODH_PRODUCT_NAME} - Dynamic Plugin SDK`, }) || compose; diff --git a/frontend/src/routes/pipelines/experiments.ts b/frontend/src/routes/pipelines/experiments.ts index 9f268a20b2..56c8566320 100644 --- a/frontend/src/routes/pipelines/experiments.ts +++ b/frontend/src/routes/pipelines/experiments.ts @@ -1,3 +1,5 @@ +import { PipelineRunType } from '~/pages/pipelines/global/runs'; + export const experimentsRootPath = '/experiments'; export const globExperimentsAll = `${experimentsRootPath}/*`; @@ -34,10 +36,13 @@ export const experimentsCloneScheduleRoute = ( export const experimentRunsRoute = ( namespace: string | undefined, experimentId: string | undefined, + runType?: PipelineRunType | null, ): string => !experimentId ? experimentsBaseRoute(namespace) - : `${experimentsBaseRoute(namespace)}/${experimentId}/runs`; + : `${experimentsBaseRoute(namespace)}/${experimentId}/runs${ + runType ? `?runType=${runType}` : '' + }`; export const experimentSchedulesRoute = ( namespace: string | undefined, diff --git a/frontend/src/routes/pipelines/global.ts b/frontend/src/routes/pipelines/global.ts index 685b43b8d0..32fa3276b2 100644 --- a/frontend/src/routes/pipelines/global.ts +++ b/frontend/src/routes/pipelines/global.ts @@ -1,3 +1,5 @@ +import { PipelineRunType } from '~/pages/pipelines/global/runs'; + const globNamespace = ':namespace'; export const globNamespaceAll = `/${globNamespace}?/*`; @@ -31,8 +33,11 @@ export const routePipelineVersionRunsNamespace = ( namespace: string, pipelineId: string, versionId: string, + runType?: PipelineRunType, ): string => - `${routePipelinesNamespace(namespace)}/${routePipelineVersionRuns(pipelineId, versionId)}`; + `${routePipelinesNamespace(namespace)}/${routePipelineVersionRuns(pipelineId, versionId)}${ + runType ? `?runType=${runType}` : '' + }`; export const routePipelineRunCreateNamespacePipelinesPage = (namespace?: string): string => `${routePipelinesNamespace(namespace)}/${globPipelineRunCreate}`; export const routePipelineRunCloneNamespacePipelinesPage = ( diff --git a/frontend/src/typeHelpers.ts b/frontend/src/typeHelpers.ts index 6df5120c04..2a22a458fe 100644 --- a/frontend/src/typeHelpers.ts +++ b/frontend/src/typeHelpers.ts @@ -1,3 +1,5 @@ +import { isEnumMember } from '~/utilities/utils'; + /** * The type `{}` doesn't mean "any empty object", it means "any non-nullish value". * @@ -162,7 +164,7 @@ export type ExactlyOne = AtMostOne & AtLeastOne; export const isInEnum = (e: T) => (token: unknown): token is T[keyof T] => - Object.values(e).includes(token as T[keyof T]); + isEnumMember(token, e); /** * Pick keys from enum types diff --git a/frontend/src/utilities/NavData.tsx b/frontend/src/utilities/NavData.tsx index 3192d0967c..d8522e0b59 100644 --- a/frontend/src/utilities/NavData.tsx +++ b/frontend/src/utilities/NavData.tsx @@ -28,10 +28,9 @@ export type NavDataGroup = NavDataCommon & { export type NavDataItem = NavDataHref | NavDataGroup; -export const isNavDataHref = (navData: NavDataItem): navData is NavDataHref => - !!(navData as NavDataHref).href; +export const isNavDataHref = (navData: NavDataItem): navData is NavDataHref => 'href' in navData; export const isNavDataGroup = (navData: NavDataItem): navData is NavDataGroup => - !!(navData as NavDataGroup).children; + 'children' in navData; const useAreaCheck = (area: SupportedArea, success: T[]): T[] => useIsAreaAvailable(area).status ? success : []; diff --git a/frontend/src/utilities/__tests__/utils.spec.ts b/frontend/src/utilities/__tests__/utils.spec.ts index 04f4135917..736c302d85 100644 --- a/frontend/src/utilities/__tests__/utils.spec.ts +++ b/frontend/src/utilities/__tests__/utils.spec.ts @@ -1,4 +1,4 @@ -import { asEnumMember, isEnumMember } from '~/utilities/utils'; +import { asEnumMember, enumIterator, isEnumMember } from '~/utilities/utils'; enum Test { first = '1st', @@ -87,3 +87,21 @@ describe('isEnumMember', () => { expect(isEnumMember(null, TestNumeric)).toBe(false); }); }); + +describe('enumIterator', () => { + it('should iterate over enum values', () => { + expect(enumIterator(Test)).toEqual([ + ['first', '1st'], + ['second', '2nd'], + ]); + expect(enumIterator(TestMixed)).toEqual([ + ['first', 1], + ['second', '2nd'], + ['third', '3rd'], + ]); + expect(enumIterator(TestNumeric)).toEqual([ + ['first', 0], + ['second', 1], + ]); + }); +}); diff --git a/frontend/src/utilities/useAcceleratorProfileState.ts b/frontend/src/utilities/useAcceleratorProfileState.ts index f777fc594b..239de80f51 100644 --- a/frontend/src/utilities/useAcceleratorProfileState.ts +++ b/frontend/src/utilities/useAcceleratorProfileState.ts @@ -10,7 +10,7 @@ import { TolerationOperator, } from '~/types'; import useGenericObjectState, { GenericObjectState } from '~/utilities/useGenericObjectState'; -import { getAcceleratorProfileCount } from '~/utilities/utils'; +import { getAcceleratorProfileCount, isEnumMember } from '~/utilities/utils'; export type AcceleratorProfileState = { acceleratorProfile?: AcceleratorProfileKind; @@ -65,9 +65,8 @@ const useAcceleratorProfileState = ( } else { // check if there is accelerator usage in the container // this is to handle the case where the accelerator is disabled, deleted, or empty - const containerResourceAttributes = Object.values(ContainerResourceAttributes) as string[]; const possibleAcceleratorRequests = Object.entries(resources.requests ?? {}) - .filter(([key]) => !containerResourceAttributes.includes(key)) + .filter(([key]) => !isEnumMember(key, ContainerResourceAttributes)) .map(([key, value]) => ({ identifier: key, count: value })); if (possibleAcceleratorRequests.length > 0) { // check if they are just using the nvidia.com/gpu diff --git a/frontend/src/utilities/useDraggableTable.ts b/frontend/src/utilities/useDraggableTable.ts index 6fa8540dcb..9ea56d503f 100644 --- a/frontend/src/utilities/useDraggableTable.ts +++ b/frontend/src/utilities/useDraggableTable.ts @@ -125,7 +125,7 @@ const useDraggableTable = ( return; } - const curListItem = (evt.target as HTMLTableSectionElement).closest('tr'); + const curListItem = evt.target instanceof HTMLElement ? evt.target.closest('tr') : null; if ( !curListItem || !bodyRef.current.contains(curListItem) || @@ -159,7 +159,7 @@ const useDraggableTable = ( ); const onDragEnd = React.useCallback((evt) => { - const target = evt.currentTarget as HTMLTableRowElement; + const target = evt.currentTarget; target.classList.remove(styles.modifiers.ghostRow); target.setAttribute('aria-pressed', 'false'); setDraggedItemId(''); diff --git a/frontend/src/utilities/useWatchNotebooksForUsers.tsx b/frontend/src/utilities/useWatchNotebooksForUsers.tsx index 13a09e4f75..9451a16d0c 100644 --- a/frontend/src/utilities/useWatchNotebooksForUsers.tsx +++ b/frontend/src/utilities/useWatchNotebooksForUsers.tsx @@ -54,10 +54,13 @@ const useWatchNotebooksForUsers = ( setLoadError(undefined); } - const newNotebooks = successes.reduce((acc, { value }) => { - acc[value.name] = value.data; - return acc; - }, {} as UsernameMap); + const newNotebooks = successes.reduce>( + (acc, { value }) => { + acc[value.name] = value.data; + return acc; + }, + {}, + ); setNotebooks((prevState) => ({ ...prevState, ...newNotebooks })); setLoaded(true); }) diff --git a/frontend/src/utilities/utils.ts b/frontend/src/utilities/utils.ts index 2b7def86e2..f29ef82928 100644 --- a/frontend/src/utilities/utils.ts +++ b/frontend/src/utilities/utils.ts @@ -140,6 +140,7 @@ export const getDashboardMainContainer = (): HTMLElement => document.getElementById(DASHBOARD_MAIN_CONTAINER_ID) || document.body; export const isHTMLInputElement = (object: unknown): object is HTMLInputElement => + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions (object as Partial).value !== undefined; export const normalizeBetween = (value: number, min?: number, max?: number): number => { @@ -159,13 +160,19 @@ export const getAcceleratorProfileCount = ( resources: ContainerResources, ): number => Number(resources.requests?.[acceleratorProfile.spec.identifier] ?? 0); +export const enumIterator = (e: T): [keyof T, T[keyof T]][] => + Object.entries(e) + .filter(([key]) => Number.isNaN(Number(key))) + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + .map(([key, value]) => [key as keyof T, value]); + export const asEnumMember = ( - member: T[keyof T] | string | number | null, + member: T[keyof T] | string | number | undefined | null, e: T, ): T[keyof T] | null => (isEnumMember(member, e) ? member : null); export const isEnumMember = ( - member: T[keyof T] | string | number | null, + member: T[keyof T] | string | number | undefined | unknown | null, e: T, ): member is T[keyof T] => { if (member != null) { diff --git a/manifests/overlays/apps/apps-onprem/starburst-enterprise/starburstenterprise-app.yaml b/manifests/overlays/apps/apps-onprem/starburst-enterprise/starburstenterprise-app.yaml index b66bae1285..7aae61c2bb 100644 --- a/manifests/overlays/apps/apps-onprem/starburst-enterprise/starburstenterprise-app.yaml +++ b/manifests/overlays/apps/apps-onprem/starburst-enterprise/starburstenterprise-app.yaml @@ -30,7 +30,7 @@ spec: - Previously installed and configured Kubernetes, including access to **kubectl**. - An editor suitable for editing YAML files. - Your SEP (Starburst Enterprise) license file. - - The latest OpenShift Container Platform (OCP) client for your platform as described in the [OpenShift documentation](https://docs.openshift.com/container-platform/4.10/cli_reference/openshift_cli/getting-started-cli.html) and the **oc** executable copied into your path, usually **/usr/local/bin.** + - The latest OpenShift Container Platform (OCP) client for your platform as described in the [OpenShift documentation](https://docs.openshift.com/container-platform/latest/cli_reference/openshift_cli/getting-started-cli.html) and the **oc** executable copied into your path, usually **/usr/local/bin.** ## Installation