diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d95072290d..2bd3bb70ed 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,8 +1,6 @@ name: Pull request on: pull_request: - branches: [ main, f/** ] - jobs: Tests: runs-on: ubuntu-latest diff --git a/.github/workflows/vuln_scan.yml b/.github/workflows/vuln_scan.yml index 30405e857c..a37d12b178 100644 --- a/.github/workflows/vuln_scan.yml +++ b/.github/workflows/vuln_scan.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner for filesystem - uses: aquasecurity/trivy-action@0.11.2 + uses: aquasecurity/trivy-action@0.13.1 with: scan-type: 'fs' scan-ref: '.' diff --git a/docs/process-definition/branches.md b/docs/process-definition/branches.md index 889edfb78f..77ab38363a 100644 --- a/docs/process-definition/branches.md +++ b/docs/process-definition/branches.md @@ -17,7 +17,7 @@ There are really two types of branches. - Core branches (like `main` and `incubation`) - [Bot branches](#bot-branches) -Every _new_ commit needs to come from a fork through a PR. We don't allow for pushing new content directly through our flows. New docs file, new code change, and even fixing a typo needs a PR from your fork to get into our repository. +Every _new_ commit needs to come from a fork through a PR. We don't allow for pushing new content directly through our flows. New docs file, new code change, and even fixing a typo needs a PR from your fork to get into our repository. This ensures automated tests pass before the change is merged. With that said, there are really 3 types of flows that utilize both fork branches and Upstream branches. @@ -33,6 +33,19 @@ There is only ever 1 `main` and 1 `incubation` branch. Feature branches start wi Read more on git tags & releases in our [release documentation]. +## Merging upstream branches + +Understanding the commit history on an upstream branch is important. Therefore when merging an upstream branch into another, your PR branch must follow the pattern `merge-`. + +For example when merging `f/some-feature` into `incubation`, name your branch `merge-f/some-feature`. This will result in a commit message on the `incubation` branch of `Merge pull request # from /merge-f/some-feature` when the PR is merged. + +Use the following steps to create a PR when merging upstream branches: + +- `git checkout -b merge- ` +- `git pull --no-rebase upstream ` +- Resolve all conflicts then post a PR. +- If the target branch is `main`, wait for the PR to be approved. For all other target branches, apply the `approved` and `lgtm` labels to the PR once all checks pass. + ## Main > aka "The Stable Branch" diff --git a/frontend/.eslintrc b/frontend/.eslintrc index 723794959b..b03965d33a 100755 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -69,6 +69,10 @@ "group": ["~/components/table/**", "!~/components/table/useTableColumnSort"], "message": "Read from '~/components/table' instead." }, + { + "group": ["~/concepts/area/**"], + "message": "Read from '~/concepts/area' instead." + }, { "group": ["~/components/table/useTableColumnSort"], "message": "The data will be sorted in the table, don't use this hook outside of '~/components/table' repo. For more information, please check the props of the Table component." diff --git a/frontend/src/__mocks__/mockDashboardConfig.ts b/frontend/src/__mocks__/mockDashboardConfig.ts index d62f24bc09..d9f63cefc4 100644 --- a/frontend/src/__mocks__/mockDashboardConfig.ts +++ b/frontend/src/__mocks__/mockDashboardConfig.ts @@ -1,5 +1,4 @@ -import { DashboardConfig } from '~/types'; -import { KnownLabels } from '~/k8sTypes'; +import { DashboardConfigKind, KnownLabels } from '~/k8sTypes'; type MockDashboardConfigType = { disableInfo?: boolean; @@ -11,6 +10,7 @@ type MockDashboardConfigType = { disableAppLauncher?: boolean; disableUserManagement?: boolean; disableProjects?: boolean; + disablePipelines?: boolean; disableModelServing?: boolean; disableCustomServingRuntimes?: boolean; }; @@ -27,7 +27,8 @@ export const mockDashboardConfig = ({ disableProjects = false, disableModelServing = false, disableCustomServingRuntimes = false, -}: MockDashboardConfigType): DashboardConfig => ({ + disablePipelines = false, +}: MockDashboardConfigType): DashboardConfigKind => ({ apiVersion: 'opendatahub.io/v1alpha', kind: 'OdhDashboardConfig', metadata: { @@ -51,7 +52,7 @@ export const mockDashboardConfig = ({ disableProjects, disableModelServing, disableCustomServingRuntimes, - disablePipelines: false, + disablePipelines, disableProjectSharing: false, disableBiasMetrics: false, disablePerformanceMetrics: false, diff --git a/frontend/src/__mocks__/mockDscStatus.ts b/frontend/src/__mocks__/mockDscStatus.ts new file mode 100644 index 0000000000..c3428b5b5c --- /dev/null +++ b/frontend/src/__mocks__/mockDscStatus.ts @@ -0,0 +1,20 @@ +import { DataScienceClusterKindStatus } from '~/k8sTypes'; +import { StackComponent } from '~/concepts/areas/types'; + +type MockDscStatus = { + installedComponents?: DataScienceClusterKindStatus['installedComponents']; +}; + +export const mockDscStatus = ({ + installedComponents, +}: MockDscStatus): DataScienceClusterKindStatus => ({ + conditions: [], + installedComponents: Object.values(StackComponent).reduce( + (acc, component) => ({ + ...acc, + [component]: installedComponents?.[component] ?? false, + }), + {}, + ), + phase: 'Ready', +}); diff --git a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts index 585f794d0d..6c0afbd519 100644 --- a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts +++ b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts @@ -74,3 +74,19 @@ test('Legacy Serving Runtime', async ({ page }) => { await expect(firstRow).toHaveClass('pf-m-expanded'); await expect(secondRow).not.toHaveClass('pf-m-expanded'); }); + +test('Add model server', async ({ page }) => { + await page.goto(navigateToStory('pages-modelserving-servingruntimelist', 'add-server')); + + // wait for page to load + await page.waitForSelector('text=Add server'); + + // test that you can not submit on empty + await expect(await page.getByRole('button', { name: 'Add', exact: true })).toBeDisabled(); + + // test filling in minimum required fields + await page.getByLabel('Model server name *').fill('Test Server Name'); + await page.locator('#serving-runtime-template-selection').click(); + await page.getByText('New OVMS Server').click(); + await expect(await page.getByRole('button', { name: 'Add', exact: true })).toBeEnabled(); +}); diff --git a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx index d9cc3ad0b6..45fdf5b94a 100644 --- a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx +++ b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx @@ -152,3 +152,14 @@ export const DeployModel: StoryObj = { await userEvent.click(canvas.getAllByText('Deploy model', { selector: 'button' })[0]); }, }; + +export const AddServer: StoryObj = { + render: Template, + + play: async ({ canvasElement }) => { + // load page and wait until settled + const canvas = within(canvasElement); + await canvas.findByText('ovms', undefined, { timeout: 5000 }); + await userEvent.click(canvas.getByText('Add server', { selector: 'button' })); + }, +}; diff --git a/frontend/src/api/prometheus/serving.ts b/frontend/src/api/prometheus/serving.ts index cd17ff5e04..7e89488ce7 100644 --- a/frontend/src/api/prometheus/serving.ts +++ b/frontend/src/api/prometheus/serving.ts @@ -13,11 +13,10 @@ import { RefreshIntervalTitle, TimeframeTitle, } from '~/pages/modelServing/screens/types'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; import { ResponsePredicate } from '~/api/prometheus/usePrometheusQueryRange'; import useRefreshInterval from '~/utilities/useRefreshInterval'; import { QueryTimeframeStep, RefreshIntervalValue } from '~/pages/modelServing/screens/const'; -import usePerformanceMetricsEnabled from '~/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import useQueryRangeResourceData from './useQueryRangeResourceData'; export const useModelServingMetrics = ( @@ -36,8 +35,10 @@ export const useModelServingMetrics = ( refresh: () => void; } => { const [end, setEnd] = React.useState(lastUpdateTime); - const [biasMetricsEnabled] = useBiasMetricsEnabled(); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); + const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; + const performanceMetricsAreaAvailable = useIsAreaAvailable( + SupportedArea.PERFORMANCE_METRICS, + ).status; const defaultResponsePredicate = React.useCallback( (data) => data.result?.[0]?.values || [], @@ -49,7 +50,7 @@ export const useModelServingMetrics = ( >((data) => data.result || [], []); const serverRequestCount = useQueryRangeResourceData( - performanceMetricsEnabled && type === PerformanceMetricType.SERVER, + performanceMetricsAreaAvailable && type === PerformanceMetricType.SERVER, (queries as { [key in ServerMetricType]: string })[ServerMetricType.REQUEST_COUNT], end, timeframe, @@ -60,7 +61,7 @@ export const useModelServingMetrics = ( const serverAverageResponseTime = useQueryRangeResourceData( - performanceMetricsEnabled && type === PerformanceMetricType.SERVER, + performanceMetricsAreaAvailable && type === PerformanceMetricType.SERVER, (queries as { [key in ServerMetricType]: string })[ServerMetricType.AVG_RESPONSE_TIME], end, timeframe, @@ -70,7 +71,7 @@ export const useModelServingMetrics = ( ); const serverCPUUtilization = useQueryRangeResourceData( - performanceMetricsEnabled && type === PerformanceMetricType.SERVER, + performanceMetricsAreaAvailable && type === PerformanceMetricType.SERVER, (queries as { [key in ServerMetricType]: string })[ServerMetricType.CPU_UTILIZATION], end, timeframe, @@ -80,7 +81,7 @@ export const useModelServingMetrics = ( ); const serverMemoryUtilization = useQueryRangeResourceData( - performanceMetricsEnabled && type === PerformanceMetricType.SERVER, + performanceMetricsAreaAvailable && type === PerformanceMetricType.SERVER, (queries as { [key in ServerMetricType]: string })[ServerMetricType.MEMORY_UTILIZATION], end, timeframe, @@ -90,7 +91,7 @@ export const useModelServingMetrics = ( ); const modelRequestSuccessCount = useQueryRangeResourceData( - performanceMetricsEnabled && type === PerformanceMetricType.MODEL, + performanceMetricsAreaAvailable && type === PerformanceMetricType.MODEL, (queries as { [key in ModelMetricType]: string })[ModelMetricType.REQUEST_COUNT_SUCCESS], end, timeframe, @@ -100,7 +101,7 @@ export const useModelServingMetrics = ( ); const modelRequestFailedCount = useQueryRangeResourceData( - performanceMetricsEnabled && type === PerformanceMetricType.MODEL, + performanceMetricsAreaAvailable && type === PerformanceMetricType.MODEL, (queries as { [key in ModelMetricType]: string })[ModelMetricType.REQUEST_COUNT_FAILED], end, timeframe, @@ -110,7 +111,7 @@ export const useModelServingMetrics = ( ); const modelTrustyAISPD = useQueryRangeResourceData( - biasMetricsEnabled && type === PerformanceMetricType.MODEL, + biasMetricsAreaAvailable && type === PerformanceMetricType.MODEL, (queries as { [key in ModelMetricType]: string })[ModelMetricType.TRUSTY_AI_SPD], end, timeframe, @@ -121,7 +122,7 @@ export const useModelServingMetrics = ( ); const modelTrustyAIDIR = useQueryRangeResourceData( - biasMetricsEnabled && type === PerformanceMetricType.MODEL, + biasMetricsAreaAvailable && type === PerformanceMetricType.MODEL, (queries as { [key in ModelMetricType]: string })[ModelMetricType.TRUSTY_AI_DIR], end, timeframe, diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index 2d8e49dfdf..7650a348b5 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -18,6 +18,7 @@ import { useUser } from '~/redux/selectors'; import { DASHBOARD_MAIN_CONTAINER_ID } from '~/utilities/const'; import useDetectUser from '~/utilities/useDetectUser'; import ProjectsContextProvider from '~/concepts/projects/ProjectsContext'; +import AreaContextProvider from '~/concepts/areas/AreaContext'; import Header from './Header'; import AppRoutes from './AppRoutes'; import NavSidebar from './NavSidebar'; @@ -89,25 +90,27 @@ const App: React.FC = () => { dashboardConfig, }} > - setNotificationsOpen(!notificationsOpen)} />} - sidebar={isAllowed ? : undefined} - notificationDrawer={ setNotificationsOpen(false)} />} - isNotificationDrawerExpanded={notificationsOpen} - mainContainerId={DASHBOARD_MAIN_CONTAINER_ID} - > - - - - - - - - - - + + setNotificationsOpen(!notificationsOpen)} />} + sidebar={isAllowed ? : undefined} + notificationDrawer={ setNotificationsOpen(false)} />} + isNotificationDrawerExpanded={notificationsOpen} + mainContainerId={DASHBOARD_MAIN_CONTAINER_ID} + > + + + + + + + + + + + ); }; diff --git a/frontend/src/app/AppContext.ts b/frontend/src/app/AppContext.ts index f3fddb7cc4..b3409ef535 100644 --- a/frontend/src/app/AppContext.ts +++ b/frontend/src/app/AppContext.ts @@ -1,15 +1,16 @@ import * as React from 'react'; -import { BuildStatus, DashboardConfig } from '~/types'; +import { DashboardConfigKind } from '~/k8sTypes'; +import { BuildStatus } from '~/types'; type AppContextProps = { buildStatuses: BuildStatus[]; - dashboardConfig: DashboardConfig; + dashboardConfig: DashboardConfigKind; }; const defaultAppContext: AppContextProps = { buildStatuses: [], // At runtime dashboardConfig is never null -- DO NOT DO THIS usually - dashboardConfig: null as unknown as DashboardConfig, + dashboardConfig: null as unknown as DashboardConfigKind, }; export const AppContext = React.createContext(defaultAppContext); diff --git a/frontend/src/app/NavSidebar.tsx b/frontend/src/app/NavSidebar.tsx index c2b9d4b189..e3e217b335 100644 --- a/frontend/src/app/NavSidebar.tsx +++ b/frontend/src/app/NavSidebar.tsx @@ -1,9 +1,7 @@ import React from 'react'; import { Link, useLocation } from 'react-router-dom'; import { Nav, NavExpandable, NavItem, NavList, PageSidebar } from '@patternfly/react-core'; -import { getNavBarData, isNavDataGroup, NavDataGroup, NavDataHref } from '~/utilities/NavData'; -import { useUser } from '~/redux/selectors'; -import { useAppContext } from './AppContext'; +import { isNavDataGroup, NavDataGroup, NavDataHref, useBuildNavData } from '~/utilities/NavData'; const checkLinkActiveStatus = (pathname: string, href: string) => href.split('/')[1] === pathname.split('/')[1]; @@ -45,24 +43,27 @@ const NavGroup: React.FC<{ item: NavDataGroup; pathname: string }> = ({ item, pa }; const NavSidebar: React.FC = () => { - const { dashboardConfig } = useAppContext(); const routerLocation = useLocation(); - const { isAdmin } = useUser(); - const userNavData = getNavBarData(isAdmin, dashboardConfig); - const nav = ( - + const userNavData = useBuildNavData(); + + return ( + + + {userNavData.map((item) => + isNavDataGroup(item) ? ( + + ) : ( + + ), + )} + + + } + theme="dark" + /> ); - return ; }; export default NavSidebar; diff --git a/frontend/src/app/useApplicationSettings.tsx b/frontend/src/app/useApplicationSettings.tsx index d0189aba1d..a91773688b 100644 --- a/frontend/src/app/useApplicationSettings.tsx +++ b/frontend/src/app/useApplicationSettings.tsx @@ -1,18 +1,18 @@ import * as React from 'react'; -import { DashboardConfig } from '~/types'; +import { DashboardConfigKind } from '~/k8sTypes'; import { POLL_INTERVAL } from '~/utilities/const'; import { useDeepCompareMemoize } from '~/utilities/useDeepCompareMemoize'; import { fetchDashboardConfig } from '~/services/dashboardConfigService'; import useTimeBasedRefresh from './useTimeBasedRefresh'; export const useApplicationSettings = (): { - dashboardConfig: DashboardConfig | null; + dashboardConfig: DashboardConfigKind | null; loaded: boolean; loadError: Error | undefined; } => { const [loaded, setLoaded] = React.useState(false); const [loadError, setLoadError] = React.useState(); - const [dashboardConfig, setDashboardConfig] = React.useState(null); + const [dashboardConfig, setDashboardConfig] = React.useState(null); const setRefreshMarker = useTimeBasedRefresh(); React.useEffect(() => { @@ -55,7 +55,7 @@ export const useApplicationSettings = (): { }; }, [setRefreshMarker]); - const retConfig = useDeepCompareMemoize(dashboardConfig); + const retConfig = useDeepCompareMemoize(dashboardConfig); return { dashboardConfig: retConfig, loaded, loadError }; }; diff --git a/frontend/src/components/SimpleDropdownSelect.tsx b/frontend/src/components/SimpleDropdownSelect.tsx index fad00f220d..f362860ade 100644 --- a/frontend/src/components/SimpleDropdownSelect.tsx +++ b/frontend/src/components/SimpleDropdownSelect.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Dropdown, DropdownItem, DropdownToggle } from '@patternfly/react-core'; +import { Dropdown, DropdownItem, DropdownToggle, Truncate } from '@patternfly/react-core'; import './SimpleDropdownSelect.scss'; export type SimpleDropdownOption = { @@ -44,7 +44,7 @@ const SimpleDropdownSelect: React.FC = ({ className={isFullWidth ? 'full-width' : undefined} onToggle={() => setOpen(!open)} > - <>{selectedLabel} + } dropdownItems={options diff --git a/frontend/src/concepts/areas/AreaComponent.tsx b/frontend/src/concepts/areas/AreaComponent.tsx new file mode 100644 index 0000000000..cd0ffa8805 --- /dev/null +++ b/frontend/src/concepts/areas/AreaComponent.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import NotFound from '~/pages/NotFound'; +import { SupportedArea } from './types'; +import useIsAreaAvailable from './useIsAreaAvailable'; + +type AreaComponentProps = { + /** What area do you need to be active to show the `children` */ + area: SupportedArea; + /** Lazy rendered children, keeps from executing the content until we know it's available */ + children: () => React.ReactNode; + /** Optionally, if the children are the whole page context, render a 404 page */ + isFullPage?: boolean; +}; + +/** + * Allows for you to wrap an area in the middle of your JSX. + */ +const AreaComponent: React.FC = ({ area, children, isFullPage }) => { + const isAvailable = useIsAreaAvailable(area).status; + + if (!children || typeof children !== 'function') { + // Typescript needs a gate to stop "what if it is null" + // TODO: This should be fixed when we enforce strict mode in TS + return null; + } + + if (isAvailable) { + return <>{children()}; + } + + return isFullPage ? : null; +}; + +/** + * Allows you to lock down a component at the definition level. + * + * Use-case: This component is only for feature X, if area X is not enabled, we don't want this to + * render. + * + * Example usage: + * ``` + * const MyAreaComponent = conditionalArea(SupportedArea.YOUR_AREA)((props) => ) + * ``` + * Notes: + * - Wrap the function definition + * - Don't use React.FC, it is handled internally + */ +export const conditionalArea = + (area: SupportedArea, isFullPage?: boolean) => + (Component: React.FC) => { + const ConditionalArea = (props: Props) => ( + + {() => } + + ); + + return ConditionalArea; + }; + +export default AreaComponent; diff --git a/frontend/src/concepts/areas/AreaContext.tsx b/frontend/src/concepts/areas/AreaContext.tsx new file mode 100644 index 0000000000..9e88a4bb9c --- /dev/null +++ b/frontend/src/concepts/areas/AreaContext.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { Alert, Bullseye, Spinner } from '@patternfly/react-core'; +import useFetchDscStatus from '~/concepts/areas/useFetchDscStatus'; +import { DataScienceClusterKindStatus } from '~/k8sTypes'; + +type AreaContextState = { + /** + * If value is `null`: + * Using the v1 Operator, no status to pull + * TODO: Remove when we no longer want to support v1 + */ + dscStatus: DataScienceClusterKindStatus | null; +}; + +export const AreaContext = React.createContext({ + dscStatus: null, +}); + +type AreaContextProps = { + children: React.ReactNode; +}; + +const AreaContextProvider: React.FC = ({ children }) => { + const [dscStatus, loaded, error] = useFetchDscStatus(); + + if (error) { + return ( + + {error.message} + + ); + } + + if (!loaded) { + return ( + + + + ); + } + + return {children}; +}; + +export default AreaContextProvider; diff --git a/frontend/src/concepts/areas/__tests__/const.spec.ts b/frontend/src/concepts/areas/__tests__/const.spec.ts new file mode 100644 index 0000000000..e334961048 --- /dev/null +++ b/frontend/src/concepts/areas/__tests__/const.spec.ts @@ -0,0 +1,54 @@ +import { SupportedAreasStateMap } from '~/concepts/areas/const'; +import { SupportedArea } from '~/concepts/areas'; + +describe('Verify const stability', () => { + const computeTestFunc = (map: Partial) => { + const hasSuccessfulReliantAreaInternal = ( + key: SupportedArea, + passedArea: SupportedArea[] = [], + ): boolean => { + const state = map[key]; + + if (state?.reliantAreas) { + const updatedPassedArea = [...passedArea, key]; + return state.reliantAreas.every((v) => + updatedPassedArea.includes(v) + ? false + : hasSuccessfulReliantAreaInternal(v, updatedPassedArea), + ); + } + + return true; + }; + return hasSuccessfulReliantAreaInternal; + }; + const hasSuccessfulReliantArea = computeTestFunc(SupportedAreasStateMap); + + it('utility should fail on reliant areas', () => { + const state: Partial = { + [SupportedArea.DS_PROJECTS_VIEW]: { + featureFlags: [], + reliantAreas: [SupportedArea.DS_PROJECTS_PERMISSIONS], + }, + [SupportedArea.DS_PROJECTS_PERMISSIONS]: { + featureFlags: [], + reliantAreas: [SupportedArea.DS_PROJECTS_VIEW], + }, + }; + const hasSuccessfulReliantAreaForTest = computeTestFunc(state); + + expect(hasSuccessfulReliantAreaForTest(SupportedArea.DS_PROJECTS_PERMISSIONS)).toBe(false); + }); + + it('should not have circular reliant areas', () => { + const list = Object.keys(SupportedAreasStateMap); + list.forEach( + (v) => + hasSuccessfulReliantArea(v as SupportedArea) || + expect(`SupportedArea => ${v} has a circle reference in reliantAreas`).toBe( + 'No issues in SupportedAreasStateMap', + ), + ); + expect(list.length > 0).toBe(true); + }); +}); diff --git a/frontend/src/concepts/areas/__tests__/utils.spec.ts b/frontend/src/concepts/areas/__tests__/utils.spec.ts new file mode 100644 index 0000000000..70af261f3a --- /dev/null +++ b/frontend/src/concepts/areas/__tests__/utils.spec.ts @@ -0,0 +1,201 @@ +import { isAreaAvailable, SupportedArea } from '~/concepts/areas'; +import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; +import { mockDscStatus } from '~/__mocks__/mockDscStatus'; +import { StackComponent } from '~/concepts/areas/types'; +import { SupportedAreasStateMap } from '~/concepts/areas/const'; + +describe('isAreaAvailable', () => { + describe('v1 Operator (deprecated)', () => { + it('should enable component (flag true)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: false }).spec, + null, + ); + + expect(isAvailable.status).toBe(true); + expect(isAvailable.featureFlags).toEqual({ ['disablePipelines']: 'on' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toBe(null); + }); + + it('should disable component (flag false)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: true }).spec, + null, + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ ['disablePipelines']: 'off' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toBe(null); + }); + + it('should enable area when not a feature flag component', () => { + const isAvailable = isAreaAvailable( + SupportedArea.WORKBENCHES, + mockDashboardConfig({}).spec, + null, + ); + + expect(isAvailable.status).toBe(true); + expect(isAvailable.featureFlags).toBe(null); + expect(isAvailable.reliantAreas).toEqual({ [SupportedArea.DS_PROJECTS_VIEW]: true }); + expect(isAvailable.requiredComponents).toBe(null); + }); + }); + + describe('v2 Operator', () => { + describe('flags and cluster states', () => { + it('should enable area (flag true, cluster true)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: false }).spec, + mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: true } }), + ); + + expect(isAvailable.status).toBe(true); + expect(isAvailable.featureFlags).toEqual({ ['disablePipelines']: 'on' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.DS_PIPELINES]: true }); + }); + + it('should disable area (flag true, cluster false)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: false }).spec, + mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: false } }), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ ['disablePipelines']: 'on' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.DS_PIPELINES]: false }); + }); + + it('should disable area (flag false, cluster true)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: true }).spec, + mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: true } }), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ ['disablePipelines']: 'off' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.DS_PIPELINES]: true }); + }); + + it('should disable area (flag false, cluster false)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: true }).spec, + mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: false } }), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ ['disablePipelines']: 'off' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.DS_PIPELINES]: false }); + }); + + it('should enable area (no flag, cluster true)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.WORKBENCHES, + mockDashboardConfig({}).spec, + mockDscStatus({ installedComponents: { [StackComponent.WORKBENCHES]: true } }), + ); + + expect(isAvailable.status).toBe(true); + expect(isAvailable.featureFlags).toBe(null); + expect(isAvailable.reliantAreas).toEqual({ [SupportedArea.DS_PROJECTS_VIEW]: true }); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.WORKBENCHES]: true }); + }); + + it('should disable area (no flag, cluster false)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.WORKBENCHES, + mockDashboardConfig({}).spec, + mockDscStatus({ installedComponents: { [StackComponent.WORKBENCHES]: false } }), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toBe(null); + expect(isAvailable.reliantAreas).toEqual({ [SupportedArea.DS_PROJECTS_VIEW]: true }); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.WORKBENCHES]: false }); + }); + }); + + /** + * These tests rely on Model Serving being in a specific configuration, we may need to replace + * these tests if these become obsolete. + */ + describe('reliantAreas', () => { + it('should enable area if at least one reliant area is enabled', () => { + // Make sure this test is valid + expect(SupportedAreasStateMap[SupportedArea.MODEL_SERVING].reliantAreas).toEqual([ + SupportedArea.K_SERVE, + SupportedArea.MODEL_MESH, + ]); + + // Test both reliant areas + const isAvailableReliantModelMesh = isAreaAvailable( + SupportedArea.MODEL_SERVING, + mockDashboardConfig({ disableModelServing: false }).spec, + mockDscStatus({ installedComponents: { [StackComponent.MODEL_MESH]: true } }), + ); + + expect(isAvailableReliantModelMesh.status).toBe(true); + expect(isAvailableReliantModelMesh.featureFlags).toEqual({ ['disableModelServing']: 'on' }); + expect(isAvailableReliantModelMesh.reliantAreas).toEqual({ + [SupportedArea.K_SERVE]: false, + [SupportedArea.MODEL_MESH]: true, + }); + expect(isAvailableReliantModelMesh.requiredComponents).toBe(null); + + const isAvailableReliantKServe = isAreaAvailable( + SupportedArea.MODEL_SERVING, + mockDashboardConfig({ disableModelServing: false }).spec, + mockDscStatus({ installedComponents: { [StackComponent.K_SERVE]: true } }), + ); + + expect(isAvailableReliantKServe.status).toBe(true); + expect(isAvailableReliantKServe.featureFlags).toEqual({ ['disableModelServing']: 'on' }); + expect(isAvailableReliantKServe.reliantAreas).toEqual({ + [SupportedArea.K_SERVE]: true, + [SupportedArea.MODEL_MESH]: false, + }); + expect(isAvailableReliantKServe.requiredComponents).toBe(null); + }); + + it('should disable area if reliant areas are all disabled', () => { + // Make sure this test is valid + expect(SupportedAreasStateMap[SupportedArea.MODEL_SERVING].reliantAreas).toEqual([ + SupportedArea.K_SERVE, + SupportedArea.MODEL_MESH, + ]); + + // Test both areas disabled + const isAvailable = isAreaAvailable( + SupportedArea.MODEL_SERVING, + mockDashboardConfig({ disableModelServing: false }).spec, + mockDscStatus({ + installedComponents: { + [StackComponent.K_SERVE]: false, + [StackComponent.MODEL_MESH]: false, + }, + }), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ ['disableModelServing']: 'on' }); + expect(isAvailable.reliantAreas).toEqual({ + [SupportedArea.K_SERVE]: false, + [SupportedArea.MODEL_MESH]: false, + }); + expect(isAvailable.requiredComponents).toBe(null); + }); + }); + }); +}); diff --git a/frontend/src/concepts/areas/const.ts b/frontend/src/concepts/areas/const.ts new file mode 100644 index 0000000000..07310ce3a9 --- /dev/null +++ b/frontend/src/concepts/areas/const.ts @@ -0,0 +1,59 @@ +import { StackComponent, SupportedArea, SupportedAreasState } from './types'; + +export const SupportedAreasStateMap: SupportedAreasState = { + [SupportedArea.BYON]: { + featureFlags: ['disableBYONImageStream'], + }, + [SupportedArea.CLUSTER_SETTINGS]: { + featureFlags: ['disableClusterManager'], + }, + [SupportedArea.CUSTOM_RUNTIMES]: { + featureFlags: ['disableCustomServingRuntimes'], + reliantAreas: [SupportedArea.MODEL_SERVING], + }, + [SupportedArea.DS_PIPELINES]: { + featureFlags: ['disablePipelines'], + requiredComponents: [StackComponent.DS_PIPELINES], + }, + [SupportedArea.DS_PROJECTS_VIEW]: { + featureFlags: ['disableProjects'], + }, + [SupportedArea.DS_PROJECTS_PERMISSIONS]: { + featureFlags: ['disableProjectSharing'], + reliantAreas: [SupportedArea.DS_PROJECTS_VIEW], + }, + [SupportedArea.K_SERVE]: { + //featureFlags: ['disableKServe'], // TODO: validate KServe feature flag + requiredComponents: [StackComponent.K_SERVE], + }, + [SupportedArea.MODEL_MESH]: { + //featureFlags: ['disableModelMesh'], // TODO: validate ModelMesh feature flag + requiredComponents: [StackComponent.MODEL_MESH], + }, + [SupportedArea.MODEL_SERVING]: { + featureFlags: ['disableModelServing'], + reliantAreas: [SupportedArea.K_SERVE, SupportedArea.MODEL_MESH], + }, + [SupportedArea.USER_MANAGEMENT]: { + featureFlags: ['disableUserManagement'], + }, + [SupportedArea.WORKBENCHES]: { + // featureFlags: [], // TODO: We want to disable, no flag exists today + requiredComponents: [StackComponent.WORKBENCHES], + reliantAreas: [SupportedArea.DS_PROJECTS_VIEW], + }, + [SupportedArea.BIAS_METRICS]: { + featureFlags: ['disableBiasMetrics'], + requiredComponents: [StackComponent.TRUSTY_AI], + reliantAreas: [SupportedArea.MODEL_SERVING], + }, + [SupportedArea.PERFORMANCE_METRICS]: { + featureFlags: ['disablePerformanceMetrics'], + requiredComponents: [StackComponent.MODEL_MESH], // TODO: remove when KServe support is added + reliantAreas: [SupportedArea.MODEL_SERVING], + }, + [SupportedArea.TRUSTY_AI]: { + requiredComponents: [StackComponent.TRUSTY_AI], + reliantAreas: [SupportedArea.BIAS_METRICS], + }, +}; diff --git a/frontend/src/concepts/areas/index.ts b/frontend/src/concepts/areas/index.ts new file mode 100644 index 0000000000..d4ab163880 --- /dev/null +++ b/frontend/src/concepts/areas/index.ts @@ -0,0 +1,11 @@ +/* + This section of concepts is intended to be for all things understanding our state with respect to + areas of the application and the stack components that may relate. + + This area should not bloat to support specifics of any area, this is just helpers and ways to + determine the state we are in. +*/ +export { default as AreaComponent, conditionalArea } from './AreaComponent'; +export { SupportedArea } from './types'; +export { default as useIsAreaAvailable } from './useIsAreaAvailable'; +export { isAreaAvailable } from './utils'; diff --git a/frontend/src/concepts/areas/types.ts b/frontend/src/concepts/areas/types.ts new file mode 100644 index 0000000000..eabb39b5bd --- /dev/null +++ b/frontend/src/concepts/areas/types.ts @@ -0,0 +1,95 @@ +import { EitherOrBoth } from '~/typeHelpers'; +import { DashboardCommonConfig } from '~/k8sTypes'; + +// TODO: clean up this definition / update the DashboardConfig to a better state +export type FeatureFlag = keyof Omit; + +export type IsAreaAvailableStatus = { + /** A single boolean status */ + status: boolean; + /* Each status portion broken down -- null if no check made */ + featureFlags: { [key in FeatureFlag]?: 'on' | 'off' } | null; // simplified. `disableX` flags are weird to read + reliantAreas: { [key in SupportedArea]?: boolean } | null; // only needs 1 to be true + requiredComponents: { [key in StackComponent]?: boolean } | null; +}; + +/** All areas that we need to support in some fashion or another */ +export enum SupportedArea { + /* Standalone areas */ + DS_PIPELINES = 'ds-pipelines', + // TODO: Jupyter Tile Support? (outside of feature flags today) + WORKBENCHES = 'workbenches', + // TODO: Support Applications/Tile area + // TODO: Support resources area + + /* Admin areas */ + BYON = 'bring-your-own-notebook', + CLUSTER_SETTINGS = 'cluster-settings', + USER_MANAGEMENT = 'user-management', + + /* DS Projects specific areas */ + DS_PROJECTS_PERMISSIONS = 'ds-projects-permission', + DS_PROJECTS_VIEW = 'ds-projects', + + /* Model Serving areas */ + MODEL_SERVING = 'model-serving-shell', + CUSTOM_RUNTIMES = 'custom-serving-runtimes', + K_SERVE = 'kserve', + MODEL_MESH = 'model-mesh', + BIAS_METRICS = 'bias-metrics', + PERFORMANCE_METRICS = 'performance-metrics', + TRUSTY_AI = 'trusty-ai', +} + +/** Components deployed by the Operator. Part of the DSC Status. */ +export enum StackComponent { + CODE_FLARE = 'codeflare', + DS_PIPELINES = 'data-science-pipelines-operator', + K_SERVE = 'kserve', + MODEL_MESH = 'model-mesh', + // Bug: https://github.com/opendatahub-io/opendatahub-operator/issues/641 + DASHBOARD = 'odh-dashboard', + RAY = 'ray', + WORKBENCHES = 'workbenches', + TRUSTY_AI = 'trustyai', +} + +// TODO: Support extra operators, like the pipelines operator -- maybe as a "external dependency need?" +type SupportedComponentFlagValue = { + /** + * An area can be reliant on another area being enabled. The list is "OR"-ed together. + * + * Example, Model Serving is a shell for either KServe or ModelMesh. It has no value on its own. + * It can also be a chain of reliance... example, Custom Runtimes is a Model Serving feature. + * + * TODO: support AND -- maybe double array? + */ + reliantAreas?: SupportedArea[]; +} & EitherOrBoth< + { + /** + * Refers to OdhDashboardConfig's feature flags, any number of them to be "enabled", the result + * is AND-ed. Omit to not be related to any feature flag. + * + * Note: "disable" methodology is confusing and needs to be removed + * Note: "Enabled" will mean "disable" is false + * @see https://github.com/opendatahub-io/odh-dashboard/issues/1108 + */ + featureFlags: FeatureFlag[]; + }, + { + /** + * Refers to the related stack component names. If a backend component is not installed, this + * can prevent the feature flag from enabling the item. Omit to not be reliant on a backend + * component. + */ + requiredComponents: StackComponent[]; + } +>; + +/** + * Relationships between areas and the state of the cluster. + */ +export type SupportedAreasState = { + [key in SupportedArea]: SupportedComponentFlagValue; +}; diff --git a/frontend/src/concepts/areas/useFetchDscStatus.ts b/frontend/src/concepts/areas/useFetchDscStatus.ts new file mode 100644 index 0000000000..034bbca0b3 --- /dev/null +++ b/frontend/src/concepts/areas/useFetchDscStatus.ts @@ -0,0 +1,24 @@ +import axios from 'axios'; +import useFetchState from '~/utilities/useFetchState'; +import { DataScienceClusterKindStatus } from '~/k8sTypes'; + +/** + * Should only return `null` when on v1 Operator. + */ +const fetchDscStatus = (): Promise => { + const url = '/api/dsc/status'; + return axios + .get(url) + .then((response) => response.data) + .catch((e) => { + if (e.response.status === 404) { + // DSC is not available, assume v1 Operator + return null; + } + throw new Error(e.response.data.message); + }); +}; + +const useFetchDscStatus = () => useFetchState(fetchDscStatus, null); + +export default useFetchDscStatus; diff --git a/frontend/src/concepts/areas/useIsAreaAvailable.ts b/frontend/src/concepts/areas/useIsAreaAvailable.ts new file mode 100644 index 0000000000..4697b1909a --- /dev/null +++ b/frontend/src/concepts/areas/useIsAreaAvailable.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { useAppContext } from '~/app/AppContext'; +import { AreaContext } from '~/concepts/areas/AreaContext'; +import { useDeepCompareMemoize } from '~/utilities/useDeepCompareMemoize'; +import { IsAreaAvailableStatus, SupportedArea } from './types'; +import { isAreaAvailable } from './utils'; + +const useIsAreaAvailable = (area: SupportedArea): IsAreaAvailableStatus => { + const { dashboardConfig } = useAppContext(); + const { dscStatus } = React.useContext(AreaContext); + + const dashboardConfigSpecSafe = useDeepCompareMemoize(dashboardConfig.spec); + const dscStatusSafe = useDeepCompareMemoize(dscStatus); + + return React.useMemo( + () => isAreaAvailable(area, dashboardConfigSpecSafe, dscStatusSafe), + [area, dashboardConfigSpecSafe, dscStatusSafe], + ); +}; + +export default useIsAreaAvailable; diff --git a/frontend/src/concepts/areas/utils.ts b/frontend/src/concepts/areas/utils.ts new file mode 100644 index 0000000000..79c78611e4 --- /dev/null +++ b/frontend/src/concepts/areas/utils.ts @@ -0,0 +1,74 @@ +import { DashboardConfigKind, DataScienceClusterKindStatus } from '~/k8sTypes'; +import { IsAreaAvailableStatus, FeatureFlag, SupportedArea } from './types'; +import { SupportedAreasStateMap } from './const'; + +type FlagState = { [flag in FeatureFlag]?: boolean }; +const getFlags = (dashboardConfigSpec: DashboardConfigKind['spec']): FlagState => { + const flags = dashboardConfigSpec.dashboardConfig; + + // TODO: Improve to be a list of items + const isFeatureFlag = (key: string, value: unknown): key is FeatureFlag => + typeof value === 'boolean'; + + return { + ...Object.keys(flags).reduce((flagState, key) => { + const value = flags[key as FeatureFlag]; + if (isFeatureFlag(key, value)) { + flagState[key] = key.startsWith('disable') ? !value : value; + } + return flagState; + }, {}), + // TODO: support this better; improve types + // notebookController: dashboardConfigSpec.notebookController?.enabled ?? false, + }; +}; + +export const isAreaAvailable = ( + area: SupportedArea, + dashboardConfigSpec: DashboardConfigKind['spec'], + dscStatus: DataScienceClusterKindStatus | null, +): IsAreaAvailableStatus => { + const { featureFlags, requiredComponents, reliantAreas } = SupportedAreasStateMap[area]; + + const reliantAreasState = reliantAreas + ? reliantAreas.reduce( + (areaStates, area) => ({ + ...areaStates, + [area]: isAreaAvailable(area, dashboardConfigSpec, dscStatus).status, + }), + {}, + ) + : null; + // Only need one to be true to work + const reliantAreaValues = reliantAreasState ? Object.values(reliantAreasState) : []; + const hasMetReliantAreas = reliantAreaValues.length > 0 ? reliantAreaValues.some((v) => v) : true; + + const flagState = getFlags(dashboardConfigSpec); + const featureFlagState = featureFlags + ? featureFlags.reduce( + (acc, flag) => ({ ...acc, [flag]: flagState[flag] ? 'on' : 'off' }), + {}, + ) + : null; + const hasMetFeatureFlags = featureFlagState + ? Object.values(featureFlagState).every((v) => v === 'on') + : true; + + const requiredComponentsState = + requiredComponents && dscStatus + ? requiredComponents.reduce( + (acc, component) => ({ ...acc, [component]: dscStatus.installedComponents[component] }), + {}, + ) + : null; + const hasMetRequiredComponents = requiredComponentsState + ? Object.values(requiredComponentsState).every((v) => v) + : true; + + return { + status: hasMetReliantAreas && hasMetFeatureFlags && hasMetRequiredComponents, + reliantAreas: reliantAreasState, + featureFlags: featureFlagState, + requiredComponents: requiredComponentsState, + }; +}; diff --git a/frontend/src/concepts/explainability/ExplainabilityContext.tsx b/frontend/src/concepts/explainability/ExplainabilityContext.tsx index e0a23d794a..bc8abd760a 100644 --- a/frontend/src/concepts/explainability/ExplainabilityContext.tsx +++ b/frontend/src/concepts/explainability/ExplainabilityContext.tsx @@ -12,7 +12,7 @@ import useFetchState, { FetchStateCallbackPromise, NotReadyError, } from '~/utilities/useFetchState'; -import useBiasMetricsEnabled from './useBiasMetricsEnabled'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; type ExplainabilityContextData = { refresh: () => Promise; @@ -127,10 +127,10 @@ const useFetchContextData = (apiState: TrustyAPIState): ExplainabilityContextDat }; const useFetchBiasMetricConfigs = (apiState: TrustyAPIState): FetchState => { - const [biasMetricsEnabled] = useBiasMetricsEnabled(); + const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; const callback = React.useCallback>( (opts) => { - if (!biasMetricsEnabled) { + if (!biasMetricsAreaAvailable) { return Promise.reject(new NotReadyError('Bias metrics is not enabled')); } if (!apiState.apiAvailable) { @@ -143,7 +143,7 @@ const useFetchBiasMetricConfigs = (apiState: TrustyAPIState): FetchState { - const { - dashboardConfig: { - spec: { - dashboardConfig: { disableBiasMetrics }, - }, - }, - } = useAppContext(); - - return [featureFlagEnabled(disableBiasMetrics)]; -}; - -export default useBiasMetricsEnabled; diff --git a/frontend/src/concepts/explainability/useBiasMetricsInstalled.ts b/frontend/src/concepts/explainability/useBiasMetricsInstalled.ts deleted file mode 100644 index a11d71d773..0000000000 --- a/frontend/src/concepts/explainability/useBiasMetricsInstalled.ts +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; -import { ExplainabilityContext } from '~/concepts/explainability/ExplainabilityContext'; - -const useBiasMetricsInstalled = () => { - const [biasMetricsEnabled] = useBiasMetricsEnabled(); - const { hasCR } = React.useContext(ExplainabilityContext); - - return [biasMetricsEnabled && hasCR]; -}; - -export default useBiasMetricsInstalled; diff --git a/frontend/src/concepts/explainability/useDoesTrustyAICRExist.ts b/frontend/src/concepts/explainability/useDoesTrustyAICRExist.ts new file mode 100644 index 0000000000..fb465a28c7 --- /dev/null +++ b/frontend/src/concepts/explainability/useDoesTrustyAICRExist.ts @@ -0,0 +1,12 @@ +import React from 'react'; +import { ExplainabilityContext } from '~/concepts/explainability/ExplainabilityContext'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; + +const useDoesTrustyAICRExist = () => { + const trustyAIAreaAvailable = useIsAreaAvailable(SupportedArea.TRUSTY_AI).status; + const { hasCR } = React.useContext(ExplainabilityContext); + + return [trustyAIAreaAvailable && hasCR]; +}; + +export default useDoesTrustyAICRExist; diff --git a/frontend/src/concepts/explainability/useTrustyAIAPIRoute.ts b/frontend/src/concepts/explainability/useTrustyAIAPIRoute.ts index b861437710..ad83f043e1 100644 --- a/frontend/src/concepts/explainability/useTrustyAIAPIRoute.ts +++ b/frontend/src/concepts/explainability/useTrustyAIAPIRoute.ts @@ -7,14 +7,14 @@ import useFetchState, { import { getTrustyAIAPIRoute } from '~/api/'; import { RouteKind } from '~/k8sTypes'; import { FAST_POLL_INTERVAL } from '~/utilities/const'; -import useBiasMetricsEnabled from './useBiasMetricsEnabled'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; type State = string | null; const useTrustyAIAPIRoute = (hasCR: boolean, namespace: string): FetchState => { - const [biasMetricsEnabled] = useBiasMetricsEnabled(); + const trustyAIAreaAvailable = useIsAreaAvailable(SupportedArea.TRUSTY_AI).status; const callback = React.useCallback>( (opts) => { - if (!biasMetricsEnabled) { + if (!trustyAIAreaAvailable) { return Promise.reject(new NotReadyError('Bias metrics is not enabled')); } @@ -32,7 +32,7 @@ const useTrustyAIAPIRoute = (hasCR: boolean, namespace: string): FetchState => { - const [biasMetricsEnabled] = useBiasMetricsEnabled(); + const trustyAIAreaAvailable = useIsAreaAvailable(SupportedArea.TRUSTY_AI).status; const callback = React.useCallback>( (opts) => { - if (!biasMetricsEnabled) { + if (!trustyAIAreaAvailable) { return Promise.reject(new NotReadyError('Bias metrics is not enabled')); } @@ -48,7 +48,7 @@ const useTrustyAINamespaceCR = (namespace: string): FetchState => { throw e; }); }, - [namespace, biasMetricsEnabled], + [namespace, trustyAIAreaAvailable], ); const [isStarting, setIsStarting] = React.useState(false); diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx index ac4bb06fe5..fd21f192fe 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx @@ -12,6 +12,7 @@ import { relativeDuration } from '~/utilities/time'; import { asTimestamp, DetailItem, + isEmptyDateKF, renderDetailItems, } from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/utils'; type PipelineRunTabDetailsProps = { @@ -24,7 +25,6 @@ const PipelineRunTabDetails: React.FC = ({ workflowName, }) => { const { namespace, project } = usePipelinesAPI(); - if (!pipelineRunKF || !workflowName) { return ( @@ -62,10 +62,11 @@ const PipelineRunTabDetails: React.FC = ({ { key: 'Workflow name', value: workflowName }, { key: 'Created at', value: asTimestamp(new Date(pipelineRunKF.created_at)) }, { - key: 'Started at', - value: asTimestamp(new Date(pipelineRunKF.scheduled_at || pipelineRunKF.created_at)), + key: 'Finished at', + value: isEmptyDateKF(pipelineRunKF.finished_at) + ? 'N/A' + : asTimestamp(new Date(pipelineRunKF.finished_at)), }, - { key: 'Finished at', value: asTimestamp(new Date(pipelineRunKF.finished_at)) }, { key: 'Duration', value: relativeDuration(getRunDuration(pipelineRunKF)) }, ]; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/utils.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/utils.tsx index b4b18c6cca..be441c93f1 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/utils.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/utils.tsx @@ -9,6 +9,7 @@ import { TimestampFormat, } from '@patternfly/react-core'; import { GlobeAmericasIcon } from '@patternfly/react-icons'; +import { DateTimeKF } from '~/concepts/pipelines/kfTypes'; export type DetailItem = { key: string; @@ -43,3 +44,8 @@ export const asTimestamp = (date: Date): React.ReactNode => ( ); + +export const isEmptyDateKF = (date: DateTimeKF): boolean => { + const INVALID_TIMESTAMP = '1970-01-01T00:00:00Z'; + return date === INVALID_TIMESTAMP ? true : false; +}; diff --git a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx index 855b8c8f24..ad0016404d 100644 --- a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx +++ b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx @@ -16,7 +16,9 @@ import ViewPipelineServerModal from '~/concepts/pipelines/content/ViewPipelineSe import useSyncPreferredProject from '~/concepts/projects/useSyncPreferredProject'; import useManageElyraSecret from '~/concepts/pipelines/context/useManageElyraSecret'; import { deleteServer } from '~/concepts/pipelines/utils'; +import { conditionalArea, SupportedArea } from '~/concepts/areas'; import usePipelineAPIState, { PipelineAPIState } from './usePipelineAPIState'; + import usePipelineNamespaceCR, { dspaLoaded, hasServerTimedOut } from './usePipelineNamespaceCR'; import usePipelinesAPIRoute from './usePipelinesAPIRoute'; @@ -49,10 +51,10 @@ type PipelineContextProviderProps = { namespace: string; }; -export const PipelineContextProvider: React.FC = ({ - children, - namespace, -}) => { +export const PipelineContextProvider = conditionalArea( + SupportedArea.DS_PIPELINES, + true, +)(({ children, namespace }) => { const { projects } = React.useContext(ProjectsContext); const project = projects.find(byName(namespace)) ?? null; useSyncPreferredProject(project); @@ -109,7 +111,7 @@ export const PipelineContextProvider: React.FC = ( {children} ); -}; +}); type UsePipelinesAPI = PipelineAPIState & { /** The contextual namespace */ diff --git a/frontend/src/concepts/pipelines/context/PipelinesContextWorkaround.tsx b/frontend/src/concepts/pipelines/context/PipelinesContextWorkaround.tsx deleted file mode 100644 index 13edad62bd..0000000000 --- a/frontend/src/concepts/pipelines/context/PipelinesContextWorkaround.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file is immediately deprecated, this is for a small fix for the next release and will - * be fixed by https://github.com/opendatahub-io/odh-dashboard/issues/2010 - */ -import * as React from 'react'; -import axios from 'axios'; -import { Bullseye, Spinner } from '@patternfly/react-core'; -import { DataScienceClusterKindStatus } from '~/k8sTypes'; -import useFetchState from '~/utilities/useFetchState'; -import { PipelineContextProvider as PipelineContextProviderActual } from './PipelinesContext'; - -/** - * Should only return `null` when on v1 Operator. - */ -const fetchDscStatus = (): Promise => { - const url = '/api/dsc/status'; - return axios - .get(url) - .then((response) => response.data) - .catch((e) => { - if (e.response.status === 404) { - // DSC is not available, assume v1 Operator - return null; - } - throw new Error(e.response.data.message); - }); -}; - -const useFetchDscStatus = () => useFetchState(fetchDscStatus, null); - -/** @deprecated - replaced by https://github.com/opendatahub-io/odh-dashboard/issues/2010 */ -export const PipelineContextProviderWorkaround: React.FC< - React.ComponentProps -> = ({ children, ...props }) => { - const [dscStatus, loaded] = useFetchDscStatus(); - - if (!loaded) { - return ( - - - - ); - } - - if (dscStatus && !dscStatus.installedComponents?.['data-science-pipelines-operator']) { - // eslint-disable-next-line no-console - console.log('Not rendering DS Pipelines Context because there is no backing component.'); - return <>{children}; - } - - return {children}; -}; diff --git a/frontend/src/concepts/pipelines/context/index.ts b/frontend/src/concepts/pipelines/context/index.ts index d9e8a6a3b5..98c15164e0 100644 --- a/frontend/src/concepts/pipelines/context/index.ts +++ b/frontend/src/concepts/pipelines/context/index.ts @@ -6,4 +6,3 @@ export { ViewServerModal, PipelineServerTimedOut, } from './PipelinesContext'; -export { PipelineContextProviderWorkaround } from './PipelinesContextWorkaround'; diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 73dbd2df44..386ce00648 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -1,12 +1,12 @@ import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils'; import { AWS_KEYS } from '~/pages/projects/dataConnections/const'; +import { StackComponent } from '~/concepts/areas/types'; import { PodAffinity, NotebookContainer, PodToleration, Volume, ContainerResources, - DashboardCommonConfig, NotebookSize, GpuSettingString, TolerationSettings, @@ -753,7 +753,32 @@ export type TemplateParameter = { required: boolean; }; -// New specification of DashboardConfig for pass through to the UI, we will have both types until we refactor the backend calls +export type DashboardCommonConfig = { + enablement: boolean; + disableInfo: boolean; + disableSupport: boolean; + disableClusterManager: boolean; + disableTracking: boolean; + disableBYONImageStream: boolean; + disableISVBadges: boolean; + disableAppLauncher: boolean; + disableUserManagement: boolean; + disableProjects: boolean; + disableModelServing: boolean; + disableProjectSharing: boolean; + disableCustomServingRuntimes: boolean; + disablePipelines: boolean; + disableBiasMetrics: boolean; + disablePerformanceMetrics: boolean; +}; + +export type OperatorStatus = { + /** Operator is installed and will be cloned to the namespace on creation */ + available: boolean; + /** Has a detection gone underway or is the available a static default */ + queriedForStatus: boolean; +}; + export type DashboardConfigKind = K8sResourceCommon & { spec: { dashboardConfig: DashboardCommonConfig; @@ -767,12 +792,22 @@ export type DashboardConfigKind = K8sResourceCommon & { enabled: boolean; pvcSize?: string; notebookNamespace?: string; + /** @deprecated - Use AcceleratorProfiles */ gpuSetting?: GpuSettingString; notebookTolerationSettings?: TolerationSettings; }; templateOrder?: string[]; templateDisablement?: string[]; }; + /** + * TODO: Make this its own API; it's not part of the CRD + * Faux status object -- computed by the service account + */ + status: { + dependencyOperators: { + redhatOpenshiftPipelines: OperatorStatus; + }; + }; }; export type AcceleratorKind = K8sResourceCommon & { @@ -802,19 +837,9 @@ export type K8sResourceListResult> }; }; -type ComponentNames = - | 'codeflare' - | 'data-science-pipelines-operator' - | 'kserve' - | 'model-mesh' - // Bug: https://github.com/opendatahub-io/opendatahub-operator/issues/641 - | 'odh-dashboard' - | 'ray' - | 'workbenches'; - /** We don't need or should ever get the full kind, this is the status section */ export type DataScienceClusterKindStatus = { conditions: K8sCondition[]; - installedComponents: { [key in ComponentNames]?: boolean }; + installedComponents: { [key in StackComponent]?: boolean }; phase?: string; }; diff --git a/frontend/src/pages/clusterSettings/ClusterSettings.tsx b/frontend/src/pages/clusterSettings/ClusterSettings.tsx index f8a20e6be3..2f4f09813c 100644 --- a/frontend/src/pages/clusterSettings/ClusterSettings.tsx +++ b/frontend/src/pages/clusterSettings/ClusterSettings.tsx @@ -113,7 +113,7 @@ const ClusterSettings: React.FC = () => { return ( { return ( diff --git a/frontend/src/pages/modelServing/ModelServingRoutes.tsx b/frontend/src/pages/modelServing/ModelServingRoutes.tsx index 74cd912ed1..ceefc80b20 100644 --- a/frontend/src/pages/modelServing/ModelServingRoutes.tsx +++ b/frontend/src/pages/modelServing/ModelServingRoutes.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { Navigate, Route } from 'react-router-dom'; import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; import ModelServingExplainabilityWrapper from '~/pages/modelServing/screens/metrics/ModelServingExplainabilityWrapper'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import BiasConfigurationBreadcrumbPage from './screens/metrics/BiasConfigurationBreadcrumbPage'; import GlobalModelMetricsPage from './screens/metrics/GlobalModelMetricsPage'; import ModelServingContextProvider from './ModelServingContext'; @@ -12,7 +12,7 @@ import useModelMetricsEnabled from './useModelMetricsEnabled'; const ModelServingRoutes: React.FC = () => { const [modelMetricsEnabled] = useModelMetricsEnabled(); - const [biasMetricsEnabled] = useBiasMetricsEnabled(); + const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; //TODO: Split route to project and mount provider here. This will allow you to load data when model switching is later implemented. return ( @@ -24,7 +24,7 @@ const ModelServingRoutes: React.FC = () => { } /> }> } /> - {biasMetricsEnabled && ( + {biasMetricsAreaAvailable && ( } /> )} diff --git a/frontend/src/pages/modelServing/customServingRuntimes/useCustomServingRuntimesEnabled.ts b/frontend/src/pages/modelServing/customServingRuntimes/useCustomServingRuntimesEnabled.ts index 7efdb450cc..20b6ce36b1 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/useCustomServingRuntimesEnabled.ts +++ b/frontend/src/pages/modelServing/customServingRuntimes/useCustomServingRuntimesEnabled.ts @@ -1,18 +1,6 @@ -import { useAppContext } from '~/app/AppContext'; -import { featureFlagEnabled } from '~/utilities/utils'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; -const useCustomServingRuntimesEnabled = (): boolean => { - const { - dashboardConfig: { - spec: { - dashboardConfig: { disableModelServing, disableCustomServingRuntimes }, - }, - }, - } = useAppContext(); - - return ( - featureFlagEnabled(disableModelServing) && featureFlagEnabled(disableCustomServingRuntimes) - ); -}; +const useCustomServingRuntimesEnabled = (): boolean => + useIsAreaAvailable(SupportedArea.CUSTOM_RUNTIMES).status; export default useCustomServingRuntimesEnabled; diff --git a/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx b/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx index 6d1d1fe044..b131ec7cac 100644 --- a/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx +++ b/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx @@ -12,7 +12,7 @@ const ModelServingGlobal: React.FC = () => { return ( { const enabledTabs = useMetricsPageEnabledTabs(); const { biasMetricConfigs, loaded } = useExplainabilityModelData(); - const [biasMetricsInstalled] = useBiasMetricsInstalled(); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); + const [biasMetricsInstalled] = useDoesTrustyAICRExist(); + const performanceMetricsAreaAvailable = useIsAreaAvailable( + SupportedArea.PERFORMANCE_METRICS, + ).status; const { tab } = useParams<{ tab: MetricsTabKeys }>(); const navigate = useNavigate(); @@ -46,7 +48,7 @@ const MetricsPageTabs: React.FC = () => { role="region" className="odh-tabs-fix" > - {performanceMetricsEnabled && ( + {performanceMetricsAreaAvailable && ( Endpoint performance} diff --git a/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts b/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts index 124b59ba1d..abbbc8e679 100644 --- a/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts +++ b/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts @@ -1,15 +1,16 @@ -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { MetricsTabKeys } from './types'; -import usePerformanceMetricsEnabled from './usePerformanceMetricsEnabled'; const useMetricsPageEnabledTabs = () => { const enabledTabs: MetricsTabKeys[] = []; - const [biasMetricsEnabled] = useBiasMetricsEnabled(); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); - if (performanceMetricsEnabled) { + const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; + const performanceMetricsAreaAvailable = useIsAreaAvailable( + SupportedArea.PERFORMANCE_METRICS, + ).status; + if (performanceMetricsAreaAvailable) { enabledTabs.push(MetricsTabKeys.PERFORMANCE); } - if (biasMetricsEnabled) { + if (biasMetricsAreaAvailable) { enabledTabs.push(MetricsTabKeys.BIAS); } return enabledTabs; diff --git a/frontend/src/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled.ts b/frontend/src/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled.ts deleted file mode 100644 index 7753bb0e11..0000000000 --- a/frontend/src/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useAppContext } from '~/app/AppContext'; -import { featureFlagEnabled } from '~/utilities/utils'; - -const usePerformanceMetricsEnabled = () => { - const { - dashboardConfig: { - spec: { - dashboardConfig: { disablePerformanceMetrics }, - }, - }, - } = useAppContext(); - - return [featureFlagEnabled(disablePerformanceMetrics)]; -}; - -export default usePerformanceMetricsEnabled; diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTemplateSection.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTemplateSection.tsx index 2cee7c6af1..678dafa191 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTemplateSection.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTemplateSection.tsx @@ -1,5 +1,13 @@ import * as React from 'react'; -import { FormGroup, Label, Split, SplitItem, StackItem, TextInput } from '@patternfly/react-core'; +import { + FormGroup, + Label, + Split, + SplitItem, + StackItem, + TextInput, + Truncate, +} from '@patternfly/react-core'; import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; import { CreatingServingRuntimeObject } from '~/pages/modelServing/screens/types'; import { TemplateKind } from '~/k8sTypes'; @@ -31,7 +39,9 @@ const ServingRuntimeTemplateSection: React.FC - {getServingRuntimeDisplayNameFromTemplate(template)} + + {} + {isCompatibleWithAccelerator( diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx index 47a401ed4d..af5687a198 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeTableRow.tsx @@ -1,5 +1,13 @@ import * as React from 'react'; -import { Button, DropdownDirection, Icon, Skeleton, Tooltip } from '@patternfly/react-core'; + +import { + Button, + DropdownDirection, + Icon, + Skeleton, + Tooltip, + Truncate, +} from '@patternfly/react-core'; import { ActionsColumn, Tbody, Td, Tr } from '@patternfly/react-table'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; import { useNavigate } from 'react-router-dom'; @@ -9,7 +17,7 @@ import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; import { ServingRuntimeTableTabs } from '~/pages/modelServing/screens/types'; import { ProjectSectionID } from '~/pages/projects/screens/detail/types'; import { getDisplayNameFromServingRuntimeTemplate } from '~/pages/modelServing/customServingRuntimes/utils'; -import usePerformanceMetricsEnabled from '~/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import ServingRuntimeTableExpandedSection from './ServingRuntimeTableExpandedSection'; import { getInferenceServiceFromServingRuntime, isServingRuntimeTokenEnabled } from './utils'; @@ -55,7 +63,9 @@ const ServingRuntimeTableRow: React.FC = ({ const modelInferenceServices = getInferenceServiceFromServingRuntime(inferenceServices, obj); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); + const performanceMetricsAreaAvailable = useIsAreaAvailable( + SupportedArea.PERFORMANCE_METRICS, + ).status; const compoundExpandParams = ( col: ServingRuntimeTableTabs, @@ -84,7 +94,9 @@ const ServingRuntimeTableRow: React.FC = ({ obj.spec.builtInAdapter?.serverType || 'Custom Runtime'} - {getDisplayNameFromServingRuntimeTemplate(obj)} + + + = ({ title: 'Edit model server', onClick: () => onEditServingRuntime(obj), }, - ...(performanceMetricsEnabled + ...(performanceMetricsAreaAvailable ? [ { title: 'View model server metrics', diff --git a/frontend/src/pages/modelServing/screens/projects/utils.ts b/frontend/src/pages/modelServing/screens/projects/utils.ts index f38df85929..51191baf96 100644 --- a/frontend/src/pages/modelServing/screens/projects/utils.ts +++ b/frontend/src/pages/modelServing/screens/projects/utils.ts @@ -1,5 +1,10 @@ import * as React from 'react'; -import { InferenceServiceKind, SecretKind, ServingRuntimeKind } from '~/k8sTypes'; +import { + DashboardConfigKind, + InferenceServiceKind, + SecretKind, + ServingRuntimeKind, +} from '~/k8sTypes'; import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; import useGenericObjectState from '~/utilities/useGenericObjectState'; import { @@ -8,7 +13,6 @@ import { InferenceServiceStorageType, ServingRuntimeSize, } from '~/pages/modelServing/screens/types'; -import { DashboardConfig } from '~/types'; import { DEFAULT_MODEL_SERVER_SIZES } from '~/pages/modelServing/screens/const'; import { useAppContext } from '~/app/AppContext'; import { useDeepCompareMemoize } from '~/utilities/useDeepCompareMemoize'; @@ -17,7 +21,7 @@ import { getDisplayNameFromK8sResource } from '~/pages/projects/utils'; import { getDisplayNameFromServingRuntimeTemplate } from '~/pages/modelServing/customServingRuntimes/utils'; import { isCpuLimitEqual, isMemoryLimitEqual } from '~/utilities/valueUnits'; -export const getServingRuntimeSizes = (config: DashboardConfig): ServingRuntimeSize[] => { +export const getServingRuntimeSizes = (config: DashboardConfigKind): ServingRuntimeSize[] => { let sizes = config.spec.modelServerSizes || []; if (sizes.length === 0) { sizes = DEFAULT_MODEL_SERVER_SIZES; diff --git a/frontend/src/pages/modelServing/useModelMetricsEnabled.ts b/frontend/src/pages/modelServing/useModelMetricsEnabled.ts index 4886513533..7322e4b41d 100644 --- a/frontend/src/pages/modelServing/useModelMetricsEnabled.ts +++ b/frontend/src/pages/modelServing/useModelMetricsEnabled.ts @@ -1,11 +1,13 @@ -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; -import usePerformanceMetricsEnabled from './screens/metrics/usePerformanceMetricsEnabled'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; const useModelMetricsEnabled = (): [modelMetricsEnabled: boolean] => { - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); - const [biasMetricsEnabled] = useBiasMetricsEnabled(); + const performanceMetricsAreaAvailable = useIsAreaAvailable( + SupportedArea.PERFORMANCE_METRICS, + ).status; + const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; - const checkModelMetricsEnabled = () => performanceMetricsEnabled || biasMetricsEnabled; + const checkModelMetricsEnabled = () => + performanceMetricsAreaAvailable || biasMetricsAreaAvailable; return [checkModelMetricsEnabled()]; }; diff --git a/frontend/src/pages/modelServing/useModelServingEnabled.ts b/frontend/src/pages/modelServing/useModelServingEnabled.ts index 4e6b20b73a..442cb90bf6 100644 --- a/frontend/src/pages/modelServing/useModelServingEnabled.ts +++ b/frontend/src/pages/modelServing/useModelServingEnabled.ts @@ -1,16 +1,6 @@ -import { useAppContext } from '~/app/AppContext'; -import { featureFlagEnabled } from '~/utilities/utils'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; -const useModelServingEnabled = (): boolean => { - const { - dashboardConfig: { - spec: { - dashboardConfig: { disableModelServing }, - }, - }, - } = useAppContext(); - - return featureFlagEnabled(disableModelServing); -}; +const useModelServingEnabled = (): boolean => + useIsAreaAvailable(SupportedArea.MODEL_SERVING).status; export default useModelServingEnabled; diff --git a/frontend/src/pages/notebookController/screens/server/usePreferredNotebookSize.ts b/frontend/src/pages/notebookController/screens/server/usePreferredNotebookSize.ts index a5b6737267..031ded5d55 100644 --- a/frontend/src/pages/notebookController/screens/server/usePreferredNotebookSize.ts +++ b/frontend/src/pages/notebookController/screens/server/usePreferredNotebookSize.ts @@ -2,11 +2,12 @@ import * as React from 'react'; import { useAppContext } from '~/app/AppContext'; import { useNotebookUserState } from '~/utilities/notebookControllerUtils'; import { DEFAULT_NOTEBOOK_SIZES } from '~/pages/notebookController/const'; -import { DashboardConfig, NotebookSize } from '~/types'; +import { DashboardConfigKind } from '~/k8sTypes'; +import { NotebookSize } from '~/types'; import useNotification from '~/utilities/useNotification'; import { useDeepCompareMemoize } from '~/utilities/useDeepCompareMemoize'; -export const getNotebookSizes = (config: DashboardConfig): NotebookSize[] => { +export const getNotebookSizes = (config: DashboardConfigKind): NotebookSize[] => { let sizes = config.spec.notebookSizes || []; if (sizes.length === 0) { sizes = DEFAULT_NOTEBOOK_SIZES; diff --git a/frontend/src/pages/projects/ProjectDetailsContext.tsx b/frontend/src/pages/projects/ProjectDetailsContext.tsx index 333750f6b8..de071a6e0e 100644 --- a/frontend/src/pages/projects/ProjectDetailsContext.tsx +++ b/frontend/src/pages/projects/ProjectDetailsContext.tsx @@ -16,9 +16,7 @@ import useInferenceServices from '~/pages/modelServing/useInferenceServices'; import { ContextResourceData } from '~/types'; import { useContextResourceData } from '~/utilities/useContextResourceData'; import useServingRuntimeSecrets from '~/pages/modelServing/screens/projects/useServingRuntimeSecrets'; -import { PipelineContextProviderWorkaround } from '~/concepts/pipelines/context'; -import { useAppContext } from '~/app/AppContext'; -import { featureFlagEnabled } from '~/utilities/utils'; +import { PipelineContextProvider } from '~/concepts/pipelines/context'; import { byName, ProjectsContext } from '~/concepts/projects/ProjectsContext'; import InvalidProject from '~/concepts/projects/InvalidProject'; import useSyncPreferredProject from '~/concepts/projects/useSyncPreferredProject'; @@ -27,6 +25,7 @@ import useTemplateOrder from '~/pages/modelServing/customServingRuntimes/useTemp import useTemplateDisablement from '~/pages/modelServing/customServingRuntimes/useTemplateDisablement'; import { useDashboardNamespace } from '~/redux/selectors'; import { getTokenNames } from '~/pages/modelServing/utils'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { NotebookState } from './notebook/types'; import { DataConnection } from './types'; import useDataConnections from './screens/detail/data-connections/useDataConnections'; @@ -71,7 +70,6 @@ export const ProjectDetailsContext = React.createContext { - const { dashboardConfig } = useAppContext(); const { dashboardNamespace } = useDashboardNamespace(); const { namespace } = useParams<{ namespace: string }>(); const { projects } = React.useContext(ProjectsContext); @@ -150,11 +148,11 @@ const ProjectDetailsContextProvider: React.FC = () => { [namespace, serverSecrets], ); + const projectsEnabled = useIsAreaAvailable(SupportedArea.DS_PROJECTS_VIEW).status; + const pipelinesEnabled = useIsAreaAvailable(SupportedArea.DS_PIPELINES).status; + if (!project) { - if ( - featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableProjects) && - projects.length === 0 - ) { + if (projectsEnabled && projects.length === 0) { // No projects, but we do have the projects view -- navigate them so they can go through normal flows return ; } @@ -187,10 +185,10 @@ const ProjectDetailsContextProvider: React.FC = () => { groups, }} > - {featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disablePipelines) ? ( - + {pipelinesEnabled ? ( + - + ) : ( )} diff --git a/frontend/src/pages/projects/ProjectViewRoutes.tsx b/frontend/src/pages/projects/ProjectViewRoutes.tsx index 90b54728be..e50c4c40fb 100644 --- a/frontend/src/pages/projects/ProjectViewRoutes.tsx +++ b/frontend/src/pages/projects/ProjectViewRoutes.tsx @@ -11,9 +11,8 @@ import CreateRunPage from '~/concepts/pipelines/content/createRun/CreateRunPage' import CloneRunPage from '~/concepts/pipelines/content/createRun/CloneRunPage'; import ProjectModelMetricsConfigurationPage from '~/pages/modelServing/screens/projects/ProjectModelMetricsConfigurationPage'; import ProjectModelMetricsPage from '~/pages/modelServing/screens/projects/ProjectModelMetricsPage'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; -import usePerformanceMetricsEnabled from '~/pages/modelServing/screens/metrics/usePerformanceMetricsEnabled'; import ProjectInferenceExplainabilityWrapper from '~/pages/modelServing/screens/projects/ProjectInferenceExplainabilityWrapper'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import ProjectDetails from './screens/detail/ProjectDetails'; import ProjectView from './screens/projects/ProjectView'; import ProjectDetailsContextProvider from './ProjectDetailsContext'; @@ -22,8 +21,10 @@ import EditSpawnerPage from './screens/spawner/EditSpawnerPage'; const ProjectViewRoutes: React.FC = () => { const [modelMetricsEnabled] = useModelMetricsEnabled(); - const [biasMetricsEnabled] = useBiasMetricsEnabled(); - const [performanceMetricsEnabled] = usePerformanceMetricsEnabled(); + const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; + const performanceMetricsAreaAvailable = useIsAreaAvailable( + SupportedArea.PERFORMANCE_METRICS, + ).status; return ( @@ -38,13 +39,13 @@ const ProjectViewRoutes: React.FC = () => { } /> }> } /> - {biasMetricsEnabled && ( + {biasMetricsAreaAvailable && ( } /> )} } /> - {performanceMetricsEnabled && ( + {performanceMetricsAreaAvailable && ( } diff --git a/frontend/src/pages/projects/projectSettings/ProjectSettingsPage.tsx b/frontend/src/pages/projects/projectSettings/ProjectSettingsPage.tsx index c915118ded..1f53834c6a 100644 --- a/frontend/src/pages/projects/projectSettings/ProjectSettingsPage.tsx +++ b/frontend/src/pages/projects/projectSettings/ProjectSettingsPage.tsx @@ -2,17 +2,17 @@ import React from 'react'; import { PageSection, Stack, StackItem } from '@patternfly/react-core'; import ModelBiasSettingsCard from '~/pages/projects/projectSettings/ModelBiasSettingsCard'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; const ProjectSettingsPage = () => { const { currentProject } = React.useContext(ProjectDetailsContext); const namespace = currentProject.metadata.name; - const [biasMetricsEnabled] = useBiasMetricsEnabled(); + const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; return ( - {biasMetricsEnabled && ( + {biasMetricsAreaAvailable && ( diff --git a/frontend/src/pages/projects/projectSharing/utils.ts b/frontend/src/pages/projects/projectSharing/utils.ts index 0acbdffc6d..88915693ab 100644 --- a/frontend/src/pages/projects/projectSharing/utils.ts +++ b/frontend/src/pages/projects/projectSharing/utils.ts @@ -1,12 +1,7 @@ import { RoleBindingKind } from '~/k8sTypes'; -import { DashboardConfig } from '~/types'; -import { featureFlagEnabled } from '~/utilities/utils'; import { getDisplayNameFromK8sResource } from '~/pages/projects/utils'; import { ProjectSharingRBType, ProjectSharingRoleType } from './types'; -export const isProjectSharingEnabled = (dashboardConfig: DashboardConfig) => - featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableProjectSharing); - export const getRoleBindingResourceName = (roleBinding: RoleBindingKind): string => roleBinding.metadata?.name || ''; export const getRoleBindingDisplayName = (roleBinding: RoleBindingKind): string => diff --git a/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx b/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx index ee18977dfe..fe2a3e6236 100644 --- a/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx +++ b/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx @@ -7,12 +7,10 @@ import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; import { getProjectDescription, getProjectDisplayName } from '~/pages/projects/utils'; import GenericHorizontalBar from '~/pages/projects/components/GenericHorizontalBar'; import ProjectSharing from '~/pages/projects/projectSharing/ProjectSharing'; -import { useAppContext } from '~/app/AppContext'; import { useAccessReview } from '~/api'; -import { isProjectSharingEnabled } from '~/pages/projects/projectSharing/utils'; import { AccessReviewResourceAttributes } from '~/k8sTypes'; import ProjectSettingsPage from '~/pages/projects/projectSettings/ProjectSettingsPage'; -import useBiasMetricsEnabled from '~/concepts/explainability/useBiasMetricsEnabled'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import useCheckLogoutParams from './useCheckLogoutParams'; import ProjectDetailsComponents from './ProjectDetailsComponents'; @@ -23,12 +21,11 @@ const accessReviewResource: AccessReviewResourceAttributes = { }; const ProjectDetails: React.FC = () => { - const { dashboardConfig } = useAppContext(); const { currentProject } = React.useContext(ProjectDetailsContext); const displayName = getProjectDisplayName(currentProject); const description = getProjectDescription(currentProject); - const projectSharingEnabled = isProjectSharingEnabled(dashboardConfig); - const [biasMetricsEnabled] = useBiasMetricsEnabled(); + const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; + const projectSharingEnabled = useIsAreaAvailable(SupportedArea.DS_PROJECTS_PERMISSIONS).status; const { state } = useLocation(); const [allowCreate, rbacLoaded] = useAccessReview({ ...accessReviewResource, @@ -43,7 +40,7 @@ const ProjectDetails: React.FC = () => { description={description} breadcrumb={ - Data science projects} /> + Data Science Projects} /> {displayName} } @@ -56,7 +53,7 @@ const ProjectDetails: React.FC = () => { sections={[ { title: 'Components', component: , icon: }, { title: 'Permissions', component: , icon: }, - ...(biasMetricsEnabled + ...(biasMetricsAreaAvailable ? [{ title: 'Settings', component: , icon: }] : []), ]} diff --git a/frontend/src/pages/projects/screens/detail/ProjectDetailsComponents.tsx b/frontend/src/pages/projects/screens/detail/ProjectDetailsComponents.tsx index 7f5e83f8f4..9950f09c87 100644 --- a/frontend/src/pages/projects/screens/detail/ProjectDetailsComponents.tsx +++ b/frontend/src/pages/projects/screens/detail/ProjectDetailsComponents.tsx @@ -3,8 +3,9 @@ import { PageSection, Stack, StackItem } from '@patternfly/react-core'; import GenericSidebar from '~/components/GenericSidebar'; import { useAppContext } from '~/app/AppContext'; import ServingRuntimeList from '~/pages/modelServing/screens/projects/ServingRuntimeList'; -import { featureFlagEnabled } from '~/utilities/utils'; import PipelinesSection from '~/pages/projects/screens/detail/pipelines/PipelinesSection'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import useModelServingEnabled from '~/pages/modelServing/useModelServingEnabled'; import NotebooksList from './notebooks/NotebookList'; import { ProjectSectionID } from './types'; import StorageList from './storage/StorageList'; @@ -18,18 +19,21 @@ type SectionType = { const ProjectDetailsComponents: React.FC = () => { const { dashboardConfig } = useAppContext(); - const modelServingEnabled = featureFlagEnabled( - dashboardConfig.spec.dashboardConfig.disableModelServing, - ); + const workbenchesEnabled = useIsAreaAvailable(SupportedArea.WORKBENCHES).status; + const modelServingEnabled = useModelServingEnabled(); const pipelinesEnabled = - featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disablePipelines) && + useIsAreaAvailable(SupportedArea.DS_PIPELINES).status && dashboardConfig.status.dependencyOperators.redhatOpenshiftPipelines.available; const sections: SectionType[] = [ - { - id: ProjectSectionID.WORKBENCHES, - component: , - }, + ...(workbenchesEnabled + ? [ + { + id: ProjectSectionID.WORKBENCHES, + component: , + }, + ] + : []), { id: ProjectSectionID.CLUSTER_STORAGES, component: , diff --git a/frontend/src/pages/projects/screens/detail/pipelines/PipelinesSection.tsx b/frontend/src/pages/projects/screens/detail/pipelines/PipelinesSection.tsx index 0776f669fd..b3e865c791 100644 --- a/frontend/src/pages/projects/screens/detail/pipelines/PipelinesSection.tsx +++ b/frontend/src/pages/projects/screens/detail/pipelines/PipelinesSection.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Alert, Divider } from '@patternfly/react-core'; +import { Divider } from '@patternfly/react-core'; import { ProjectSectionID } from '~/pages/projects/screens/detail/types'; import { ProjectSectionTitles } from '~/pages/projects/screens/detail/const'; import DetailsSection from '~/pages/projects/screens/detail/DetailsSection'; @@ -12,28 +12,11 @@ import PipelineServerActions from '~/concepts/pipelines/content/pipelinesDetails const PipelinesSection: React.FC = () => { const { - project, pipelinesServer: { initializing, installed, timedOut }, } = usePipelinesAPI(); const [isPipelinesEmpty, setIsPipelinesEmpty] = React.useState(false); - if (!project) { - // Only possible today because of not having the API installed - // TODO: Fix in https://github.com/opendatahub-io/odh-dashboard/issues/2010 - return ( - - - - ); - } - return ( <> = ({ breadcrumbPath={[ Projects} + render={() => Data Science Projects} />, { return ( = ({ existingNotebook }) => { title={existingNotebook ? `Edit ${editNotebookDisplayName}` : 'Create workbench'} breadcrumb={ - Data science projects} /> + Data Science Projects} /> ( {displayName} diff --git a/frontend/src/pages/projects/screens/spawner/useNotebookSize.ts b/frontend/src/pages/projects/screens/spawner/useNotebookSize.ts index dfced6214d..cd0ba53ecc 100644 --- a/frontend/src/pages/projects/screens/spawner/useNotebookSize.ts +++ b/frontend/src/pages/projects/screens/spawner/useNotebookSize.ts @@ -1,11 +1,12 @@ import * as React from 'react'; import { useAppContext } from '~/app/AppContext'; -import { DashboardConfig, NotebookSize } from '~/types'; +import { DashboardConfigKind } from '~/k8sTypes'; +import { NotebookSize } from '~/types'; import useNotification from '~/utilities/useNotification'; import { useDeepCompareMemoize } from '~/utilities/useDeepCompareMemoize'; import { DEFAULT_NOTEBOOK_SIZES } from './const'; -export const getNotebookSizes = (config: DashboardConfig): NotebookSize[] => { +export const getNotebookSizes = (config: DashboardConfigKind): NotebookSize[] => { let sizes = config.spec.notebookSizes || []; if (sizes.length === 0) { sizes = DEFAULT_NOTEBOOK_SIZES; diff --git a/frontend/src/services/dashboardConfigService.ts b/frontend/src/services/dashboardConfigService.ts index 7adfe86fb9..92aac1cb8d 100644 --- a/frontend/src/services/dashboardConfigService.ts +++ b/frontend/src/services/dashboardConfigService.ts @@ -1,7 +1,7 @@ import axios from 'axios'; -import { DashboardConfig } from '~/types'; +import { DashboardConfigKind } from '~/k8sTypes'; -export const fetchDashboardConfig = (): Promise => { +export const fetchDashboardConfig = (): Promise => { const url = '/api/config'; return axios .get(url) diff --git a/frontend/src/typeHelpers.ts b/frontend/src/typeHelpers.ts index 65026cdb5f..cd1b86588a 100644 --- a/frontend/src/typeHelpers.ts +++ b/frontend/src/typeHelpers.ts @@ -66,6 +66,32 @@ type Never = { */ export type EitherNotBoth = (TypeA & Never) | (TypeB & Never); +/** + * Either TypeA properties or TypeB properties or neither of the properties -- never both. + * + * @example + * ```ts + * type MyType = EitherOrBoth<{ foo: boolean }, { bar: boolean }>; + * + * // Valid usages: + * const objA: MyType = { + * foo: true, + * }; + * const objB: MyType = { + * bar: true, + * }; + * const objBoth: MyType = { + * foo: true, + * bar: true, + * }; + * + * // TS Error -- can't omit both properties: + * const objNeither: MyType = { + * }; + * ``` + */ +export type EitherOrBoth = EitherNotBoth | (TypeA & TypeB); + /** * Either TypeA properties or TypeB properties or neither of the properties -- never both. * diff --git a/frontend/src/types.ts b/frontend/src/types.ts index c5f352f47b..1e5b648242 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,9 +1,7 @@ /* * Common types, should be kept up to date with backend types */ - import { AxiosError } from 'axios'; -import { ServingRuntimeSize } from '~/pages/modelServing/screens/types'; import { EnvironmentFromVariable } from '~/pages/projects/types'; import { ImageStreamKind, ImageStreamSpecTagType } from './k8sTypes'; import { EitherNotBoth } from './typeHelpers'; @@ -38,65 +36,14 @@ export type PrometheusQueryRangeResponse = { export type PrometheusQueryRangeResultValue = [number, string]; /** + * @deprecated - Use AcceleratorProfiles * In some YAML configs, we'll need to stringify a number -- this type just helps show it's not * "any string" as a documentation touch point. Has no baring on the type checking. */ type NumberString = string; +/** @deprecated - Use AcceleratorProfiles */ export type GpuSettingString = 'autodetect' | 'hidden' | NumberString | undefined; -export type OperatorStatus = { - /** Operator is installed and will be cloned to the namespace on creation */ - available: boolean; - /** Has a detection gone underway or is the available a static default */ - queriedForStatus: boolean; -}; - -export type DashboardConfig = K8sResourceCommon & { - spec: { - dashboardConfig: DashboardCommonConfig; - groupsConfig?: { - adminGroups: string; - allowedGroups: string; - }; - notebookSizes?: NotebookSize[]; - modelServerSizes?: ServingRuntimeSize[]; - notebookController?: { - enabled: boolean; - pvcSize?: string; - notebookNamespace?: string; - gpuSetting?: GpuSettingString; - notebookTolerationSettings?: TolerationSettings; - }; - templateOrder?: string[]; - templateDisablement?: string[]; - }; - /** Faux status object -- computed by the service account */ - status: { - dependencyOperators: { - redhatOpenshiftPipelines: OperatorStatus; - }; - }; -}; - -export type DashboardCommonConfig = { - enablement: boolean; - disableInfo: boolean; - disableSupport: boolean; - disableClusterManager: boolean; - disableTracking: boolean; - disableBYONImageStream: boolean; - disableISVBadges: boolean; - disableAppLauncher: boolean; - disableUserManagement: boolean; - disableProjects: boolean; - disableModelServing: boolean; - disableProjectSharing: boolean; - disableCustomServingRuntimes: boolean; - disablePipelines: boolean; - disableBiasMetrics: boolean; - disablePerformanceMetrics: boolean; -}; - export type NotebookControllerUserState = { user: string; lastSelectedImage: string; diff --git a/frontend/src/utilities/NavData.tsx b/frontend/src/utilities/NavData.tsx index 32916b578a..1ee1561360 100644 --- a/frontend/src/utilities/NavData.tsx +++ b/frontend/src/utilities/NavData.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; import { Icon, Split, SplitItem } from '@patternfly/react-core'; -import { DashboardConfig } from '~/types'; -import { featureFlagEnabled } from './utils'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { useAppContext } from '~/app/AppContext'; +import { useUser } from '~/redux/selectors'; type NavDataCommon = { id: string; @@ -28,121 +29,139 @@ export const isNavDataHref = (navData: NavDataItem): navData is NavDataHref => export const isNavDataGroup = (navData: NavDataItem): navData is NavDataGroup => !!(navData as NavDataGroup)?.children; -const getSettingsNav = ( - isAdmin: boolean, - dashboardConfig: DashboardConfig, -): NavDataGroup | null => { - if (!isAdmin) { - return null; - } +const useAreaCheck = (area: SupportedArea, success: T[]): T[] => + useIsAreaAvailable(area).status ? success : []; - const settingsNavs: NavDataHref[] = []; - if (featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableBYONImageStream)) { - settingsNavs.push({ - id: 'settings-notebook-images', - label: 'Notebook image settings', - href: '/notebookImages', - }); - } - - if (featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableClusterManager)) { - settingsNavs.push({ - id: 'settings-cluster-settings', - label: 'Cluster settings', - href: '/clusterSettings', - }); - } - - if ( - featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableCustomServingRuntimes) && - featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableModelServing) - ) { - settingsNavs.push({ - id: 'settings-custom-serving-runtimes', - label: 'Serving runtimes', - href: '/servingRuntimes', - }); - } - - if (featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableUserManagement)) { - settingsNavs.push({ - id: 'settings-group-settings', - label: 'User management', - href: '/groupSettings', - }); - } - - if (settingsNavs.length === 0) { - return null; - } - - return { - id: 'settings', - group: { id: 'settings', title: 'Settings' }, - children: settingsNavs, - }; -}; - -export const getNavBarData = ( - isAdmin: boolean, - dashboardConfig: DashboardConfig, -): NavDataItem[] => { - const navItems: NavDataItem[] = []; - - navItems.push({ +const useApplicationsNav = (): NavDataItem[] => [ + { id: 'applications', group: { id: 'apps', title: 'Applications' }, children: [ { id: 'apps-installed', label: 'Enabled', href: '/' }, { id: 'apps-explore', label: 'Explore', href: '/explore' }, ], - }); + }, +]; + +const useDSProjectsNav = (): NavDataItem[] => + useAreaCheck(SupportedArea.DS_PROJECTS_VIEW, [ + { id: 'dsg', label: 'Data Science Projects', href: '/projects' }, + ]); - if (featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableProjects)) { - navItems.push({ id: 'dsg', label: 'Data Science Projects', href: '/projects' }); +const useDSPipelinesNav = (): NavDataItem[] => { + const { dashboardConfig } = useAppContext(); + const isAvailable = useIsAreaAvailable(SupportedArea.DS_PIPELINES).status; + + if (!isAvailable) { + return []; } - if (featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disablePipelines)) { - const operatorAvailable = - dashboardConfig.status.dependencyOperators.redhatOpenshiftPipelines.available; + const operatorAvailable = + dashboardConfig.status.dependencyOperators.redhatOpenshiftPipelines.available; - if (operatorAvailable) { - navItems.push({ + if (operatorAvailable) { + return [ + { id: 'pipelines', group: { id: 'pipelines', title: 'Data Science Pipelines' }, children: [ { id: 'global-pipelines', label: 'Pipelines', href: '/pipelines' }, { id: 'global-pipeline-runs', label: 'Runs', href: '/pipelineRuns' }, ], - }); - } else { - navItems.push({ - id: 'pipelines', - label: ( - - Data Science Pipelines - - - - - - - ), - href: `/dependency-missing/pipelines`, - }); - } + }, + ]; } - if (featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableModelServing)) { - navItems.push({ id: 'modelServing', label: 'Model Serving', href: '/modelServing' }); - } + return [ + { + id: 'pipelines', + label: ( + + Data Science Pipelines + + + + + + + ), + href: `/dependency-missing/pipelines`, + }, + ]; +}; + +const useModelServingNav = (): NavDataItem[] => + useAreaCheck(SupportedArea.MODEL_SERVING, [ + { id: 'modelServing', label: 'Model Serving', href: '/modelServing' }, + ]); + +const useResourcesNav = (): NavDataHref[] => [ + { id: 'resources', label: 'Resources', href: '/resources' }, +]; + +const useCustomNotebooksNav = (): NavDataHref[] => + useAreaCheck(SupportedArea.BYON, [ + { + id: 'settings-notebook-images', + label: 'Notebook image settings', + href: '/notebookImages', + }, + ]); + +const useClusterSettingsNav = (): NavDataHref[] => + useAreaCheck(SupportedArea.CLUSTER_SETTINGS, [ + { + id: 'settings-cluster-settings', + label: 'Cluster settings', + href: '/clusterSettings', + }, + ]); - navItems.push({ id: 'resources', label: 'Resources', href: '/resources' }); +const useCustomRuntimesNav = (): NavDataHref[] => + useAreaCheck(SupportedArea.CUSTOM_RUNTIMES, [ + { + id: 'settings-custom-serving-runtimes', + label: 'Serving runtimes', + href: '/servingRuntimes', + }, + ]); - const settingsNav = getSettingsNav(isAdmin, dashboardConfig); - if (settingsNav) { - navItems.push(settingsNav); +const useUserManagementNav = (): NavDataHref[] => + useAreaCheck(SupportedArea.USER_MANAGEMENT, [ + { + id: 'settings-group-settings', + label: 'User management', + href: '/groupSettings', + }, + ]); + +const useSettingsNav = (): NavDataGroup[] => { + const settingsNavs: NavDataHref[] = [ + ...useCustomNotebooksNav(), + ...useClusterSettingsNav(), + ...useCustomRuntimesNav(), + ...useUserManagementNav(), + ]; + + const { isAdmin } = useUser(); + if (!isAdmin || settingsNavs.length === 0) { + return []; } - return navItems; + return [ + { + id: 'settings', + group: { id: 'settings', title: 'Settings' }, + children: settingsNavs, + }, + ]; }; + +export const useBuildNavData = (): NavDataItem[] => [ + ...useApplicationsNav(), + ...useDSProjectsNav(), + ...useDSPipelinesNav(), + ...useModelServingNav(), + ...useResourcesNav(), + ...useSettingsNav(), +]; diff --git a/frontend/src/utilities/tolerations.ts b/frontend/src/utilities/tolerations.ts index 5878656918..0e6c65cd21 100644 --- a/frontend/src/utilities/tolerations.ts +++ b/frontend/src/utilities/tolerations.ts @@ -1,7 +1,7 @@ import { Patch } from '@openshift/dynamic-plugin-sdk-utils'; import _ from 'lodash'; -import { DashboardConfig, PodToleration, TolerationSettings } from '~/types'; -import { NotebookKind } from '~/k8sTypes'; +import { PodToleration, TolerationSettings } from '~/types'; +import { DashboardConfigKind, NotebookKind } from '~/k8sTypes'; import { AcceleratorState } from './useAcceleratorState'; export type TolerationChanges = { @@ -50,7 +50,7 @@ export const determineTolerations = ( }; export const computeNotebooksTolerations = ( - dashboardConfig: DashboardConfig, + dashboardConfig: DashboardConfigKind, notebook: NotebookKind, ): TolerationChanges => { const tolerations = notebook.spec.template.spec.tolerations || []; diff --git a/frontend/src/utilities/utils.ts b/frontend/src/utilities/utils.ts index eff87fc7bd..ad03d76f67 100644 --- a/frontend/src/utilities/utils.ts +++ b/frontend/src/utilities/utils.ts @@ -1,13 +1,6 @@ import { OdhApplication, OdhDocument, OdhDocumentType } from '~/types'; import { CATEGORY_ANNOTATION, DASHBOARD_MAIN_CONTAINER_ID, ODH_PRODUCT_NAME } from './const'; -/** - * Feature flags are required in the config -- but upgrades can be mixed and omission of the property - * usually ends up being enabled. This will prevent that as a general utility. - */ -export const featureFlagEnabled = (disabledSettingState?: boolean): boolean => - disabledSettingState === false; - export const makeCardVisible = (id: string): void => { setTimeout(() => { const element = document.getElementById(id);