From 51a13305bfc7c0ea70da5115da7bf729b8417cb6 Mon Sep 17 00:00:00 2001 From: Christian Vogt Date: Fri, 21 Jun 2024 17:31:39 -0400 Subject: [PATCH] support feature flag overrides via query string --- .../cluster-settings/clusterSettingsUtils.ts | 5 +- .../src/routes/api/cluster-settings/index.ts | 2 +- backend/src/routes/api/storage/index.ts | 4 +- backend/src/utils/resourceUtils.ts | 29 +++- frontend/.eslintrc | 18 +- frontend/src/api/k8s/projects.ts | 2 +- .../src/api/prometheus/usePrometheusQuery.ts | 2 +- .../api/prometheus/usePrometheusQueryRange.ts | 2 +- frontend/src/app/App.tsx | 10 +- frontend/src/app/DevFeatureFlagsBanner.tsx | 94 +++++++++++ .../__tests__/DevFeatureFlagsBanner.spec.tsx | 80 +++++++++ .../app/__tests__/useDevFeatureFlags.spec.ts | 154 ++++++++++++++++++ frontend/src/app/useDevFeatureFlags.ts | 134 +++++++++++++++ frontend/src/concepts/areas/const.ts | 30 ++++ .../src/concepts/areas/useFetchDscStatus.ts | 2 +- .../src/concepts/areas/useFetchDsciStatus.ts | 2 +- frontend/src/concepts/areas/utils.ts | 13 +- .../content/createRun/CloneRunPage.tsx | 2 +- .../content/createRun/CreateRunPage.tsx | 4 +- .../pipeline/PipelineDetails.tsx | 4 +- .../pipelineRun/PipelineRunDetails.tsx | 7 +- .../pipelineRunJob/PipelineRunJobDetails.tsx | 3 +- .../src/concepts/pipelines/content/types.ts | 3 +- .../global/GlobalPipelineCoreDetails.tsx | 6 +- .../ArtifactDetails/ArtifactDetails.tsx | 2 +- .../compareRuns/CompareRunsPage.tsx | 2 +- .../executions/details/ExecutionDetails.tsx | 2 +- .../global/runs/GlobalPipelineVersionRuns.tsx | 4 +- .../ProjectPipelineBreadcrumbPage.tsx | 2 +- frontend/src/redux/actions/actions.ts | 2 +- frontend/src/routes/pipelines/experiments.ts | 7 +- frontend/src/routes/pipelines/global.ts | 7 +- .../src/services/acceleratorProfileService.ts | 2 +- frontend/src/services/acceleratorService.ts | 2 +- frontend/src/services/buildsService.ts | 2 +- .../src/services/clusterSettingsService.ts | 2 +- frontend/src/services/componentsServices.ts | 2 +- frontend/src/services/consoleLinksService.ts | 2 +- .../src/services/dashboardConfigService.ts | 2 +- frontend/src/services/dashboardService.ts | 2 +- frontend/src/services/docsService.ts | 2 +- frontend/src/services/envService.ts | 2 +- frontend/src/services/groupSettingsService.ts | 2 +- frontend/src/services/imagesService.ts | 2 +- frontend/src/services/impersonateService.ts | 2 +- .../services/modelRegistrySettingsService.ts | 2 +- .../src/services/notebookEventsService.ts | 2 +- frontend/src/services/notebookService.ts | 2 +- frontend/src/services/quickStartsService.ts | 2 +- frontend/src/services/roleBindingService.ts | 2 +- frontend/src/services/routeService.ts | 2 +- frontend/src/services/segmentKeyService.ts | 2 +- frontend/src/services/storageService.ts | 2 +- frontend/src/services/templateService.ts | 2 +- frontend/src/services/validateIsvService.ts | 2 +- frontend/src/types.ts | 13 +- frontend/src/utilities/axios.ts | 4 + frontend/src/utilities/useDetectUser.ts | 2 +- 58 files changed, 636 insertions(+), 69 deletions(-) create mode 100644 frontend/src/app/DevFeatureFlagsBanner.tsx create mode 100644 frontend/src/app/__tests__/DevFeatureFlagsBanner.spec.tsx create mode 100644 frontend/src/app/__tests__/useDevFeatureFlags.spec.ts create mode 100644 frontend/src/app/useDevFeatureFlags.ts create mode 100644 frontend/src/utilities/axios.ts diff --git a/backend/src/routes/api/cluster-settings/clusterSettingsUtils.ts b/backend/src/routes/api/cluster-settings/clusterSettingsUtils.ts index e93d91ae7c..44ef958369 100644 --- a/backend/src/routes/api/cluster-settings/clusterSettingsUtils.ts +++ b/backend/src/routes/api/cluster-settings/clusterSettingsUtils.ts @@ -38,7 +38,7 @@ export const updateClusterSettings = async ( notebookTolerationSettings, modelServingPlatformEnabled, } = request.body; - const dashConfig = getDashboardConfig(); + const dashConfig = getDashboardConfig(request); const isJupyterEnabled = checkJupyterEnabled(); try { if ( @@ -124,10 +124,11 @@ export const updateClusterSettings = async ( export const getClusterSettings = async ( fastify: KubeFastifyInstance, + request: FastifyRequest, ): Promise => { const coreV1Api = fastify.kube.coreV1Api; const namespace = fastify.kube.namespace; - const dashConfig = getDashboardConfig(); + const dashConfig = getDashboardConfig(request); const isJupyterEnabled = checkJupyterEnabled(); const clusterSettings: ClusterSettings = { ...DEFAULT_CLUSTER_SETTINGS, diff --git a/backend/src/routes/api/cluster-settings/index.ts b/backend/src/routes/api/cluster-settings/index.ts index 9b106fe99d..b7a7832dca 100644 --- a/backend/src/routes/api/cluster-settings/index.ts +++ b/backend/src/routes/api/cluster-settings/index.ts @@ -7,7 +7,7 @@ export default async (fastify: FastifyInstance): Promise => { fastify.get( '/', secureAdminRoute(fastify)(async (request: FastifyRequest, reply: FastifyReply) => { - return getClusterSettings(fastify) + return getClusterSettings(fastify, request) .then((res) => { return res; }) diff --git a/backend/src/routes/api/storage/index.ts b/backend/src/routes/api/storage/index.ts index f146965bad..4a93fa3894 100644 --- a/backend/src/routes/api/storage/index.ts +++ b/backend/src/routes/api/storage/index.ts @@ -14,7 +14,7 @@ export default async (fastify: FastifyInstance): Promise => { reply: FastifyReply, ) => { try { - const dashConfig = getDashboardConfig(); + const dashConfig = getDashboardConfig(request); if (dashConfig?.spec.dashboardConfig.disableS3Endpoint !== false) { reply.code(404).send('Not found'); return reply; @@ -49,7 +49,7 @@ export default async (fastify: FastifyInstance): Promise => { reply: FastifyReply, ) => { try { - const dashConfig = getDashboardConfig(); + const dashConfig = getDashboardConfig(request); if (dashConfig?.spec.dashboardConfig.disableS3Endpoint !== false) { reply.code(404).send('Not found'); return reply; diff --git a/backend/src/utils/resourceUtils.ts b/backend/src/utils/resourceUtils.ts index 3feeb60a5c..dd7465d001 100644 --- a/backend/src/utils/resourceUtils.ts +++ b/backend/src/utils/resourceUtils.ts @@ -34,6 +34,7 @@ import { getIsAppEnabled, getRouteForApplication, getRouteForClusterId } from '. import { createCustomError } from './requestUtils'; import { getDetectedAccelerators } from '../routes/api/accelerators/acceleratorUtils'; import { RecursivePartial } from '../typeHelpers'; +import { FastifyRequest } from 'fastify'; const dashboardConfigMapName = 'odh-dashboard-config'; const consoleLinksGroup = 'console.openshift.io'; @@ -548,8 +549,32 @@ export const initializeWatchedResources = (fastify: KubeFastifyInstance): void = consoleLinksWatcher = new ResourceWatcher(fastify, fetchConsoleLinks); }; -export const getDashboardConfig = (): DashboardConfig => { - return dashboardConfigWatcher.getResources()?.[0]; +const FEATURE_FLAGS_HEADER = 'x-odh-feature-flags'; + +// if inspecting feature flags, provide the request to ensure overridden feature flags are considered +export const getDashboardConfig = (request?: FastifyRequest): DashboardConfig => { + const dashboardConfig = dashboardConfigWatcher.getResources()?.[0]; + if (request) { + const flagsHeader = request.headers[FEATURE_FLAGS_HEADER]; + if (typeof flagsHeader === 'string') { + try { + const featureFlags = JSON.parse(flagsHeader); + return { + ...dashboardConfig, + spec: { + ...dashboardConfig.spec, + dashboardConfig: { + ...dashboardConfig.spec.dashboardConfig, + ...featureFlags, + }, + }, + }; + } catch { + // ignore + } + } + } + return dashboardConfig; }; export const updateDashboardConfig = (): Promise => { diff --git a/frontend/.eslintrc b/frontend/.eslintrc index cca83da5be..5c4f57f0ce 100755 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -111,18 +111,25 @@ "no-restricted-imports": [ "error", { + "paths": [ + { + "name": "^axios$", + "importNames": ["default"], + "message": "Import from `~/utilities/axios` instead." + } + ], "patterns": [ { "group": ["~/api/**"], - "message": "Read from '~/api' instead." + "message": "Import from '~/api' instead." }, { "group": ["~/components/table/**", "!~/components/table/useTableColumnSort"], - "message": "Read from '~/components/table' instead." + "message": "Import from '~/components/table' instead." }, { "group": ["~/concepts/area/**"], - "message": "Read from '~/concepts/area' instead." + "message": "Import from '~/concepts/area' instead." }, { "group": ["~/components/table/useTableColumnSort"], @@ -286,7 +293,10 @@ ], "overrides": [ { - "files": ["./src/__tests__/cypress/cypress/pages/*.ts", "./src/__tests__/cypress/cypress/tests/e2e/*.ts"], + "files": [ + "./src/__tests__/cypress/cypress/pages/*.ts", + "./src/__tests__/cypress/cypress/tests/e2e/*.ts" + ], "rules": { "no-restricted-syntax": [ "error", diff --git a/frontend/src/api/k8s/projects.ts b/frontend/src/api/k8s/projects.ts index c4b7a8df6e..8f9f800aea 100644 --- a/frontend/src/api/k8s/projects.ts +++ b/frontend/src/api/k8s/projects.ts @@ -1,4 +1,3 @@ -import axios from 'axios'; import { k8sCreateResource, k8sDeleteResource, @@ -6,6 +5,7 @@ import { K8sResourceCommon, k8sUpdateResource, } from '@openshift/dynamic-plugin-sdk-utils'; +import axios from '~/utilities/axios'; import { CustomWatchK8sResult } from '~/types'; import { K8sAPIOptions, ProjectKind } from '~/k8sTypes'; import { ProjectModel, ProjectRequestModel } from '~/api/models'; diff --git a/frontend/src/api/prometheus/usePrometheusQuery.ts b/frontend/src/api/prometheus/usePrometheusQuery.ts index baabb4e7e5..19ce8cf9b4 100644 --- a/frontend/src/api/prometheus/usePrometheusQuery.ts +++ b/frontend/src/api/prometheus/usePrometheusQuery.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import axios from 'axios'; +import axios from '~/utilities/axios'; import { PrometheusQueryResponse } from '~/types'; import useFetchState, { FetchOptions, FetchState, NotReadyError } from '~/utilities/useFetchState'; diff --git a/frontend/src/api/prometheus/usePrometheusQueryRange.ts b/frontend/src/api/prometheus/usePrometheusQueryRange.ts index 45214cfcc8..874daaf3af 100644 --- a/frontend/src/api/prometheus/usePrometheusQueryRange.ts +++ b/frontend/src/api/prometheus/usePrometheusQueryRange.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import axios from 'axios'; +import axios from '~/utilities/axios'; import useFetchState, { FetchOptions, diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index 6988612dc8..0c7922f289 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -29,8 +29,10 @@ import { useApplicationSettings } from './useApplicationSettings'; import TelemetrySetup from './TelemetrySetup'; import { logout } from './appUtils'; import QuickStarts from './QuickStarts'; +import DevFeatureFlagsBanner from './DevFeatureFlagsBanner'; import './App.scss'; +import useDevFeatureFlags from '~/app/useDevFeatureFlags'; const App: React.FC = () => { const [notificationsOpen, setNotificationsOpen] = React.useState(false); @@ -38,11 +40,13 @@ const App: React.FC = () => { const buildStatuses = useWatchBuildStatus(); const { - dashboardConfig, + dashboardConfig: dashboardConfigFromServer, loaded: configLoaded, loadError: fetchConfigError, } = useApplicationSettings(); + const { dashboardConfig, ...devFeatureFlags } = useDevFeatureFlags(dashboardConfigFromServer); + const [storageClasses] = useStorageClasses(); useDetectUser(); @@ -115,6 +119,10 @@ const App: React.FC = () => { data-testid={DASHBOARD_MAIN_CONTAINER_ID} > + diff --git a/frontend/src/app/DevFeatureFlagsBanner.tsx b/frontend/src/app/DevFeatureFlagsBanner.tsx new file mode 100644 index 0000000000..5c75c210cd --- /dev/null +++ b/frontend/src/app/DevFeatureFlagsBanner.tsx @@ -0,0 +1,94 @@ +import { Banner, Button, Checkbox, Grid, GridItem, Modal } from '@patternfly/react-core'; +import * as React from 'react'; +import { allFeatureFlags } from '~/concepts/areas/const'; +import { isFeatureFlag } from '~/concepts/areas/utils'; +import { DashboardCommonConfig } from '~/k8sTypes'; +import { DevFeatureFlags } from '~/types'; + +type Props = { dashboardConfig: Partial } & DevFeatureFlags; + +const DevdevFeatureFlagsBanner: React.FC = ({ + dashboardConfig, + devFeatureFlags, + setDevFeatureFlag, + resetDevFeatureFlags, +}) => { + const [isModalOpen, setModalOpen] = React.useState(false); + if (!devFeatureFlags) { + return null; + } + const renderdevFeatureFlags = () => ( + + {allFeatureFlags + .filter(isFeatureFlag) + .toSorted() + .map((key) => { + const value = devFeatureFlags[key] ?? dashboardConfig[key]; + return ( + + + + setDevFeatureFlag(key as keyof DashboardCommonConfig, checked) + } + /> + + {`${value ?? ''}${ + key in devFeatureFlags ? ' (overridden)' : '' + }`} + + ); + })} + + ); + return ( + <> + + Feature flags are{' '} + {' '} + in the current session.{' '} + {' '} + to reset back to default. + + setModalOpen(false)} + actions={[ + , + ]} + > + {renderdevFeatureFlags()} + + + ); +}; + +export default DevdevFeatureFlagsBanner; diff --git a/frontend/src/app/__tests__/DevFeatureFlagsBanner.spec.tsx b/frontend/src/app/__tests__/DevFeatureFlagsBanner.spec.tsx new file mode 100644 index 0000000000..4a4ddb0424 --- /dev/null +++ b/frontend/src/app/__tests__/DevFeatureFlagsBanner.spec.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import DevFeatureFlagsBanner from '~/app/DevFeatureFlagsBanner'; + +describe('DevFeatureFlagsBanner', () => { + it('should render not render if no feature flags are overridden', () => { + const result = render( + undefined} + resetDevFeatureFlags={() => undefined} + devFeatureFlags={null} + />, + ); + expect(result.container).toBeEmptyDOMElement(); + }); + + it('should render banner and open modal', () => { + const resetFn = jest.fn(); + const result = render( + undefined} + resetDevFeatureFlags={resetFn} + devFeatureFlags={{}} + />, + ); + expect(result.container).not.toBeEmptyDOMElement(); + act(() => result.getByTestId('override-feature-flags-button').click()); + result.getByTestId('dev-feature-flags-modal'); + + act(() => result.getByTestId('reset-feature-flags-button').click()); + expect(resetFn).toHaveBeenCalled(); + }); + + it('should render and set feature flags', () => { + const setFeatureFlagFn = jest.fn(); + const resetFn = jest.fn(); + const result = render( + , + ); + expect(result.container).not.toBeEmptyDOMElement(); + act(() => result.getByTestId('override-feature-flags-button').click()); + screen.getByTestId('dev-feature-flags-modal'); + + act(() => result.getByTestId('reset-feature-flags-button').click()); + expect(resetFn).toHaveBeenCalled(); + + expect(result.getByTestId('disableHome-checkbox')).toBeChecked(); + expect(result.getByTestId('disableAcceleratorProfiles-checkbox')).not.toBeChecked(); + expect(result.getByTestId('enablement-checkbox')).toBePartiallyChecked(); + + expect(result.getByTestId('disableHome-value').textContent).toBe('true (overridden)'); + expect(result.getByTestId('disableAcceleratorProfiles-value').textContent).toBe('false'); + expect(result.getByTestId('enablement-value').textContent).toBe(''); + + act(() => { + result.getByTestId('disableHome-checkbox').click(); + result.getByTestId('disableAcceleratorProfiles-checkbox').click(); + result.getByTestId('enablement-checkbox').click(); + }); + + expect(setFeatureFlagFn).toHaveBeenCalledTimes(3); + expect(setFeatureFlagFn.mock.calls).toEqual([ + ['disableHome', false], + ['disableAcceleratorProfiles', true], + ['enablement', true], + ]); + }); +}); diff --git a/frontend/src/app/__tests__/useDevFeatureFlags.spec.ts b/frontend/src/app/__tests__/useDevFeatureFlags.spec.ts new file mode 100644 index 0000000000..e33a2b9e29 --- /dev/null +++ b/frontend/src/app/__tests__/useDevFeatureFlags.spec.ts @@ -0,0 +1,154 @@ +import { merge } from 'lodash-es'; +import { act } from '@testing-library/react'; +import { useSearchParams } from 'react-router-dom'; +import { testHook } from '~/__tests__/unit/testUtils/hooks'; +import useDevFeatureFlags from '~/app/useDevFeatureFlags'; +import { useBrowserStorage } from '~/components/browserStorage'; +import { DashboardCommonConfig, DashboardConfigKind } from '~/k8sTypes'; +import axios from '~/utilities/axios'; + +jest.mock('react-router-dom', () => ({ + useSearchParams: jest.fn(() => [ + { get: jest.fn(() => null), has: jest.fn(() => false), delete: jest.fn() }, + jest.fn(), + ]), +})); +jest.mock('~/components/browserStorage', () => ({ + useBrowserStorage: jest.fn(() => [null, jest.fn()]), +})); +jest.mock('~/utilities/axios', () => ({ + __esModule: true, + default: { + defaults: { + headers: { + common: [], + }, + }, + }, +})); + +const axiosMock = jest.mocked(axios); +const useSearchParamsMock = jest.mocked(useSearchParams); +const useBrowserStorageMock = jest.mocked(useBrowserStorage); + +const mockSession = (sessionFlags: Partial | null) => { + const setSessionFn = jest.fn(); + useBrowserStorageMock.mockReturnValue([sessionFlags, setSessionFn]); + return { sessionFlags, setSessionFn }; +}; + +const mockUseSearchParams = (queryFlags: { [key in string]: boolean } | null) => { + const backing = new URLSearchParams({ + foo: 'bar', + ...(queryFlags + ? { + devFeatureFlags: Object.entries(queryFlags) + .map(([key, value]) => `${key}=${value}`) + .join(','), + } + : {}), + }); + const getFn = jest.fn((name: string) => backing.get(name)); + const hasFn = jest.fn((name: string) => backing.has(name)); + const deleteFn = jest.fn((name: string) => backing.delete(name)); + const searchParams = { + get: getFn, + has: hasFn, + delete: deleteFn, + toString: () => backing.toString(), + } as unknown as ReturnType[0]; + const setSearchParamsFn = jest.fn(); + useSearchParamsMock.mockReturnValue([searchParams, setSearchParamsFn]); + return { queryFlags, searchParams, setSearchParamsFn }; +}; + +describe('useDevFeatureFlags', () => { + it('should pass through dashboardConfig if no dev feature flags set', () => { + const dashboardConfig = {} as DashboardConfigKind; + const renderResult = testHook(useDevFeatureFlags)(dashboardConfig); + expect(renderResult.result.current.dashboardConfig).toBe(dashboardConfig); + expect(renderResult.result.current).toEqual({ + dashboardConfig, + devFeatureFlags: null, + resetDevFeatureFlags: expect.any(Function), + setDevFeatureFlag: expect.any(Function), + }); + }); + + it('should load flags from session', () => { + const { sessionFlags, setSessionFn } = mockSession({ + disableHome: true, + disableAppLauncher: false, + }); + const dashboardConfig = { + spec: { dashboardConfig: { enablement: true } }, + } as DashboardConfigKind; + const renderResult = testHook(useDevFeatureFlags)(dashboardConfig); + expect(renderResult.result.current).toEqual({ + dashboardConfig: merge(dashboardConfig, { spec: { dashboardConfig: sessionFlags } }), + devFeatureFlags: sessionFlags, + resetDevFeatureFlags: expect.any(Function), + setDevFeatureFlag: expect.any(Function), + }); + + expect(axiosMock.defaults.headers.common['x-odh-feature-flags']).toEqual( + JSON.stringify({ + disableHome: true, + disableAppLauncher: false, + }), + ); + + act(() => renderResult.result.current.resetDevFeatureFlags()); + expect(setSessionFn).toHaveBeenCalledWith(null); + act(() => renderResult.result.current.setDevFeatureFlag('disableInfo', false)); + expect(setSessionFn).toHaveBeenLastCalledWith({ + disableAppLauncher: false, + disableHome: true, + disableInfo: false, + }); + }); + + it('should load flags from query string', () => { + const { setSessionFn } = mockSession(null); + const { searchParams, setSearchParamsFn } = mockUseSearchParams({ + disableHome: true, + enablement: true, + info: false, + invalid: true, + }); + const dashboardConfig = { + spec: { dashboardConfig: { disableAppLauncher: true } }, + } as DashboardConfigKind; + const renderResult = testHook(useDevFeatureFlags)(dashboardConfig); + expect(renderResult.result.current).toEqual({ + dashboardConfig: merge(dashboardConfig, { + spec: { + dashboardConfig: { + disableAppLauncher: true, + disableHome: true, + enablement: true, + disableInfo: true, + }, + }, + }), + devFeatureFlags: { + disableHome: true, + enablement: true, + disableInfo: true, + }, + resetDevFeatureFlags: expect.any(Function), + setDevFeatureFlag: expect.any(Function), + }); + + expect(searchParams.delete).toHaveBeenCalledWith('devFeatureFlags'); + expect(setSearchParamsFn.mock.calls[0][0].toString()).toEqual( + new URLSearchParams({ foo: 'bar' }).toString(), + ); + + expect(setSessionFn).toHaveBeenCalledWith({ + disableHome: true, + enablement: true, + disableInfo: true, + }); + }); +}); diff --git a/frontend/src/app/useDevFeatureFlags.ts b/frontend/src/app/useDevFeatureFlags.ts new file mode 100644 index 0000000000..627cc05c1f --- /dev/null +++ b/frontend/src/app/useDevFeatureFlags.ts @@ -0,0 +1,134 @@ +import * as React from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useBrowserStorage } from '~/components/browserStorage'; +import { isFeatureFlag } from '~/concepts/areas/utils'; +import { DashboardCommonConfig, DashboardConfigKind } from '~/k8sTypes'; +import { DevFeatureFlags } from '~/types'; +import axios from '~/utilities/axios'; + +const PARAM_NAME = 'devFeatureFlags'; +const SESSION_KEY = 'odh-feature-flags'; +const HEADER_NAME = 'x-odh-feature-flags'; + +const capitalize = (v: string) => v.charAt(0).toUpperCase() + v.slice(1); + +/** + * Override dashboard config feature flags in the query string: eg. + * `devFeatureFlags=disableHome=false,appLauncher,support=true` + * Results in: + * - disableHome = true + * - disableAppLauncher = false + * - disableSupport = false + */ +const useDevFeatureFlags = ( + dashboardConfig?: DashboardConfigKind | null, +): { + dashboardConfig: DashboardConfigKind | null; +} & DevFeatureFlags => { + const [sessionFlags, setSessionFlags] = useBrowserStorage | null>( + SESSION_KEY, + null, + true, + true, + ); + const [searchParams, setSearchParams] = useSearchParams(); + const devFlagsParam = searchParams.get(PARAM_NAME); + + const sanitizedSessionFlags = React.useMemo | null>(() => { + if (sessionFlags) { + const entries = Object.entries(sessionFlags); + const filteredEntries = entries.filter( + ([key, value]) => isFeatureFlag(key) && typeof value === 'boolean', + ); + if (entries.length === filteredEntries.length) { + // return the original object if valid + // keep stable reference + return sessionFlags; + } + // return the sanitized object + return filteredEntries.reduce>((acc, [key, v]) => { + if (isFeatureFlag(key)) { + acc[key] = v; + } + return acc; + }, {}); + } + return null; + }, [sessionFlags]); + + const devFeatureFlags = + React.useMemo | null>(() => { + if (devFlagsParam != null) { + return devFlagsParam.split(',').reduce>((acc, v) => { + const [name, bool] = v.split('='); + if (isFeatureFlag(name)) { + acc[name] = bool === 'true'; + } else { + const fullName = `disable${capitalize(name)}`; + if (isFeatureFlag(fullName)) { + acc[fullName] = bool === 'false'; + } + } + return acc; + }, {}); + } + return null; + }, [devFlagsParam]) ?? sanitizedSessionFlags; + + React.useEffect(() => { + // assign axios default header + if (devFeatureFlags) { + axios.defaults.headers.common[HEADER_NAME] = JSON.stringify(devFeatureFlags); + } else { + delete axios.defaults.headers.common[HEADER_NAME]; + } + + // update session storage + // compares against oroginal sessionFlags object on purpose and not the sanitizedSessionFlags + if (devFeatureFlags !== sessionFlags) { + setSessionFlags(devFeatureFlags); + } + + // remove query param + if (searchParams.has(PARAM_NAME)) { + searchParams.delete(PARAM_NAME); + setSearchParams(searchParams); + } + }, [devFeatureFlags, sessionFlags, setSessionFlags, searchParams, setSearchParams]); + + const newDashboardConfig = React.useMemo(() => { + if (dashboardConfig && devFeatureFlags) { + return { + ...dashboardConfig, + spec: { + ...dashboardConfig.spec, + dashboardConfig: { + ...dashboardConfig.spec.dashboardConfig, + ...devFeatureFlags, + }, + }, + }; + } + return dashboardConfig ?? null; + }, [devFeatureFlags, dashboardConfig]); + + const resetDevFeatureFlags = React.useCallback(() => { + setSessionFlags(null); + }, [setSessionFlags]); + + const setDevFeatureFlag = React.useCallback( + (flag: keyof DashboardCommonConfig, value: boolean) => { + setSessionFlags({ ...sessionFlags, [flag]: value }); + }, + [sessionFlags, setSessionFlags], + ); + + return { + dashboardConfig: newDashboardConfig, + devFeatureFlags, + setDevFeatureFlag, + resetDevFeatureFlags, + }; +}; + +export default useDevFeatureFlags; diff --git a/frontend/src/concepts/areas/const.ts b/frontend/src/concepts/areas/const.ts index 10834bdb4a..b643127c5c 100644 --- a/frontend/src/concepts/areas/const.ts +++ b/frontend/src/concepts/areas/const.ts @@ -1,5 +1,35 @@ +import { DashboardCommonConfig } from '~/k8sTypes'; import { StackCapability, StackComponent, SupportedArea, SupportedAreasState } from './types'; +export const allFeatureFlags: string[] = Object.keys({ + enablement: false, + disableInfo: false, + disableSupport: false, + disableClusterManager: false, + disableTracking: false, + disableBYONImageStream: false, + disableISVBadges: false, + disableAppLauncher: false, + disableUserManagement: false, + disableHome: false, + disableProjects: false, + disableModelServing: false, + disableProjectSharing: false, + disableCustomServingRuntimes: false, + disablePipelines: false, + disableBiasMetrics: false, + disablePerformanceMetrics: false, + disableKServe: false, + disableKServeAuth: false, + disableKServeMetrics: false, + disableModelMesh: false, + disableAcceleratorProfiles: false, + disablePipelineExperiments: false, + disableS3Endpoint: false, + disableDistributedWorkloads: false, + disableModelRegistry: false, +} satisfies DashboardCommonConfig); + export const SupportedAreasStateMap: SupportedAreasState = { [SupportedArea.BYON]: { featureFlags: ['disableBYONImageStream'], diff --git a/frontend/src/concepts/areas/useFetchDscStatus.ts b/frontend/src/concepts/areas/useFetchDscStatus.ts index aa5f1ded45..b6362d6bb4 100644 --- a/frontend/src/concepts/areas/useFetchDscStatus.ts +++ b/frontend/src/concepts/areas/useFetchDscStatus.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import useFetchState, { FetchState } from '~/utilities/useFetchState'; import { DataScienceClusterKindStatus } from '~/k8sTypes'; diff --git a/frontend/src/concepts/areas/useFetchDsciStatus.ts b/frontend/src/concepts/areas/useFetchDsciStatus.ts index 1da899f2bb..5fb65b6d2e 100644 --- a/frontend/src/concepts/areas/useFetchDsciStatus.ts +++ b/frontend/src/concepts/areas/useFetchDsciStatus.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import useFetchState, { FetchState } from '~/utilities/useFetchState'; import { DataScienceClusterInitializationKindStatus } from '~/k8sTypes'; diff --git a/frontend/src/concepts/areas/utils.ts b/frontend/src/concepts/areas/utils.ts index 2957080141..0b712b4f56 100644 --- a/frontend/src/concepts/areas/utils.ts +++ b/frontend/src/concepts/areas/utils.ts @@ -4,20 +4,17 @@ import { DataScienceClusterKindStatus, } from '~/k8sTypes'; import { IsAreaAvailableStatus, FeatureFlag, SupportedArea } from './types'; -import { SupportedAreasStateMap } from './const'; +import { SupportedAreasStateMap, allFeatureFlags } from './const'; + +export const isFeatureFlag = (key: string): key is FeatureFlag => allFeatureFlags.includes(key); 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((acc, key) => { - const value = flags[key as FeatureFlag]; - if (isFeatureFlag(key, value)) { + ...Object.entries(flags).reduce((acc, [key, value]) => { + if (isFeatureFlag(key)) { acc[key] = key.startsWith('disable') ? !value : value; } return acc; diff --git a/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx b/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx index faf0c58670..7996a4a868 100644 --- a/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx @@ -21,7 +21,7 @@ const CloneRunPage: React.FC = ({ breadcrumbPath, contextPath }) => { title={title} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath(runType)} {run ? `Duplicate of ${run.display_name}` : 'Duplicate'} diff --git a/frontend/src/concepts/pipelines/content/createRun/CreateRunPage.tsx b/frontend/src/concepts/pipelines/content/createRun/CreateRunPage.tsx index 3930ac2091..1ce7f64b25 100644 --- a/frontend/src/concepts/pipelines/content/createRun/CreateRunPage.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/CreateRunPage.tsx @@ -16,7 +16,9 @@ const CreateRunPage: React.FC = ({ breadcrumbPath, contextPath }) => title={title} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath( + runType === PipelineRunType.SCHEDULED ? PipelineRunType.SCHEDULED : undefined, + )} {title} } diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx index 43fa1888b4..35b59ce72b 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx @@ -69,7 +69,7 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = - {breadcrumbPath} + {breadcrumbPath()} {title} } @@ -97,7 +97,7 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = - {breadcrumbPath} + {breadcrumbPath()} diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx index b4f86b7d77..3836ed9107 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx @@ -33,6 +33,7 @@ import PipelineJobReferenceName from '~/concepts/pipelines/content/PipelineJobRe import useExecutionsForPipelineRun from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/useExecutionsForPipelineRun'; import { useGetEventsByExecutionIds } from '~/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId'; import { PipelineTopology } from '~/concepts/topology'; +import { StorageStateKF } from '~/concepts/pipelines/kfTypes'; import { usePipelineRunArtifacts } from './artifacts'; import { PipelineRunDetailsTabs } from './PipelineRunDetailsTabs'; @@ -92,6 +93,9 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, ); } + const runType = + run?.storage_state === StorageStateKF.ARCHIVED ? PipelineRunType.ARCHIVED : undefined; + return ( <> @@ -128,7 +132,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, loadError={error} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath(runType)} {version ? ( diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx index a07f9a8834..abb5335da2 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx @@ -94,7 +94,7 @@ const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ loadError={error} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath(PipelineRunType.SCHEDULED)} {version ? ( {version.display_name} diff --git a/frontend/src/concepts/pipelines/content/types.ts b/frontend/src/concepts/pipelines/content/types.ts index b3c90ac2ef..039e040579 100644 --- a/frontend/src/concepts/pipelines/content/types.ts +++ b/frontend/src/concepts/pipelines/content/types.ts @@ -1,8 +1,9 @@ import * as React from 'react'; import { BreadcrumbItem } from '@patternfly/react-core'; +import { PipelineRunType } from '~/pages/pipelines/global/runs'; export type PathProps = { - breadcrumbPath: React.ReactElement[]; + breadcrumbPath: (runType?: PipelineRunType | null) => React.ReactElement[]; contextPath?: string; }; diff --git a/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx b/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx index 1b92deb875..720f13bb32 100644 --- a/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx +++ b/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx @@ -26,7 +26,7 @@ const GlobalPipelineCoreDetails: React.FC = ({ [ ( @@ -56,7 +56,7 @@ export const GlobalExperimentDetails: React.FC< [ Experiments - {getProjectDisplayName(project)} @@ -64,7 +64,7 @@ export const GlobalExperimentDetails: React.FC< , {experiment?.display_name ? ( - + {experiment.display_name} ) : ( diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx index 1ae310c802..f43a652aab 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx @@ -61,7 +61,7 @@ export const ArtifactDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPa loadError={artifactError} breadcrumb={ - {breadcrumbPath} + {breadcrumbPath()} diff --git a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx index eaa594cfd8..5f48db21af 100644 --- a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx +++ b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx @@ -21,7 +21,7 @@ const CompareRunsPage: React.FC = ({ breadcrumbPath }) => { title="" breadcrumb={ - {breadcrumbPath} + {breadcrumbPath()} Compare runs } diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx index 1090db9ce6..51037b753f 100644 --- a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx @@ -96,7 +96,7 @@ const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, co loaded breadcrumb={ - {breadcrumbPath} + {breadcrumbPath()} {displayName} } diff --git a/frontend/src/pages/pipelines/global/runs/GlobalPipelineVersionRuns.tsx b/frontend/src/pages/pipelines/global/runs/GlobalPipelineVersionRuns.tsx index 256dce6f7c..7f25baa5bc 100644 --- a/frontend/src/pages/pipelines/global/runs/GlobalPipelineVersionRuns.tsx +++ b/frontend/src/pages/pipelines/global/runs/GlobalPipelineVersionRuns.tsx @@ -29,7 +29,7 @@ const GlobalPipelineVersionRuns: PipelineCoreDetailsPageComponent = ({ breadcrum - {breadcrumbPath} + {breadcrumbPath()} {title} } @@ -46,7 +46,7 @@ const GlobalPipelineVersionRuns: PipelineCoreDetailsPageComponent = ({ breadcrum - {breadcrumbPath} + {breadcrumbPath()} diff --git a/frontend/src/pages/projects/screens/detail/pipelines/ProjectPipelineBreadcrumbPage.tsx b/frontend/src/pages/projects/screens/detail/pipelines/ProjectPipelineBreadcrumbPage.tsx index 37c751d9b6..da3400b369 100644 --- a/frontend/src/pages/projects/screens/detail/pipelines/ProjectPipelineBreadcrumbPage.tsx +++ b/frontend/src/pages/projects/screens/detail/pipelines/ProjectPipelineBreadcrumbPage.tsx @@ -19,7 +19,7 @@ const ProjectPipelineBreadcrumbPage: React.FC = ({ return ( [ Data Science Projects} diff --git a/frontend/src/redux/actions/actions.ts b/frontend/src/redux/actions/actions.ts index 5d292b1870..71392a3139 100644 --- a/frontend/src/redux/actions/actions.ts +++ b/frontend/src/redux/actions/actions.ts @@ -1,6 +1,6 @@ -import axios from 'axios'; import { ThunkAction } from 'redux-thunk'; import { Action } from 'redux'; +import axios from '~/utilities/axios'; import { Actions, AppNotification, AppState, GetUserAction, StatusResponse } from '~/redux/types'; import { AllowedUser } from '~/pages/notebookController/screens/admin/types'; diff --git a/frontend/src/routes/pipelines/experiments.ts b/frontend/src/routes/pipelines/experiments.ts index 9f268a20b2..56c8566320 100644 --- a/frontend/src/routes/pipelines/experiments.ts +++ b/frontend/src/routes/pipelines/experiments.ts @@ -1,3 +1,5 @@ +import { PipelineRunType } from '~/pages/pipelines/global/runs'; + export const experimentsRootPath = '/experiments'; export const globExperimentsAll = `${experimentsRootPath}/*`; @@ -34,10 +36,13 @@ export const experimentsCloneScheduleRoute = ( export const experimentRunsRoute = ( namespace: string | undefined, experimentId: string | undefined, + runType?: PipelineRunType | null, ): string => !experimentId ? experimentsBaseRoute(namespace) - : `${experimentsBaseRoute(namespace)}/${experimentId}/runs`; + : `${experimentsBaseRoute(namespace)}/${experimentId}/runs${ + runType ? `?runType=${runType}` : '' + }`; export const experimentSchedulesRoute = ( namespace: string | undefined, diff --git a/frontend/src/routes/pipelines/global.ts b/frontend/src/routes/pipelines/global.ts index 685b43b8d0..32fa3276b2 100644 --- a/frontend/src/routes/pipelines/global.ts +++ b/frontend/src/routes/pipelines/global.ts @@ -1,3 +1,5 @@ +import { PipelineRunType } from '~/pages/pipelines/global/runs'; + const globNamespace = ':namespace'; export const globNamespaceAll = `/${globNamespace}?/*`; @@ -31,8 +33,11 @@ export const routePipelineVersionRunsNamespace = ( namespace: string, pipelineId: string, versionId: string, + runType?: PipelineRunType, ): string => - `${routePipelinesNamespace(namespace)}/${routePipelineVersionRuns(pipelineId, versionId)}`; + `${routePipelinesNamespace(namespace)}/${routePipelineVersionRuns(pipelineId, versionId)}${ + runType ? `?runType=${runType}` : '' + }`; export const routePipelineRunCreateNamespacePipelinesPage = (namespace?: string): string => `${routePipelinesNamespace(namespace)}/${globPipelineRunCreate}`; export const routePipelineRunCloneNamespacePipelinesPage = ( diff --git a/frontend/src/services/acceleratorProfileService.ts b/frontend/src/services/acceleratorProfileService.ts index 1e381b07e5..9419176acc 100644 --- a/frontend/src/services/acceleratorProfileService.ts +++ b/frontend/src/services/acceleratorProfileService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { AcceleratorProfileKind } from '~/k8sTypes'; import { ResponseStatus } from '~/types'; diff --git a/frontend/src/services/acceleratorService.ts b/frontend/src/services/acceleratorService.ts index 814eec6906..7cbb625b83 100644 --- a/frontend/src/services/acceleratorService.ts +++ b/frontend/src/services/acceleratorService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { DetectedAccelerators } from '~/types'; export const getDetectedAccelerators = (): Promise => { diff --git a/frontend/src/services/buildsService.ts b/frontend/src/services/buildsService.ts index 3f70fea83e..9a3c6b6f08 100644 --- a/frontend/src/services/buildsService.ts +++ b/frontend/src/services/buildsService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { BuildStatus } from '~/types'; export const fetchBuildStatuses = (): Promise => { diff --git a/frontend/src/services/clusterSettingsService.ts b/frontend/src/services/clusterSettingsService.ts index 4ef275d622..5dcbdd4c8c 100644 --- a/frontend/src/services/clusterSettingsService.ts +++ b/frontend/src/services/clusterSettingsService.ts @@ -1,5 +1,5 @@ -import axios from 'axios'; import { ClusterSettingsType } from '~/types'; +import axios from '~/utilities/axios'; export const fetchClusterSettings = (): Promise => { const url = '/api/cluster-settings'; diff --git a/frontend/src/services/componentsServices.ts b/frontend/src/services/componentsServices.ts index e162f73b33..60285d7061 100644 --- a/frontend/src/services/componentsServices.ts +++ b/frontend/src/services/componentsServices.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { OdhApplication } from '~/types'; export const fetchComponents = (installed: boolean): Promise => { diff --git a/frontend/src/services/consoleLinksService.ts b/frontend/src/services/consoleLinksService.ts index 0ec8bc2163..be682a9de4 100644 --- a/frontend/src/services/consoleLinksService.ts +++ b/frontend/src/services/consoleLinksService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { ConsoleLinkKind } from '~/k8sTypes'; export const fetchConsoleLinks = (): Promise => { diff --git a/frontend/src/services/dashboardConfigService.ts b/frontend/src/services/dashboardConfigService.ts index 92aac1cb8d..498c0a4d78 100644 --- a/frontend/src/services/dashboardConfigService.ts +++ b/frontend/src/services/dashboardConfigService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { DashboardConfigKind } from '~/k8sTypes'; export const fetchDashboardConfig = (): Promise => { diff --git a/frontend/src/services/dashboardService.ts b/frontend/src/services/dashboardService.ts index c5a4e75e39..46904ca7e0 100644 --- a/frontend/src/services/dashboardService.ts +++ b/frontend/src/services/dashboardService.ts @@ -1,5 +1,5 @@ // TODO: Delete once we refactor Admin panel to support Passthrough API -import axios from 'axios'; +import axios from '~/utilities/axios'; import { DashboardConfigKind } from '~/k8sTypes'; import { DASHBOARD_CONFIG } from '~/utilities/const'; diff --git a/frontend/src/services/docsService.ts b/frontend/src/services/docsService.ts index 69be335130..726c7b81a2 100644 --- a/frontend/src/services/docsService.ts +++ b/frontend/src/services/docsService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { OdhDocument } from '~/types'; export const fetchDocs = (docType?: string): Promise => { diff --git a/frontend/src/services/envService.ts b/frontend/src/services/envService.ts index 751e08c671..1c596547b5 100644 --- a/frontend/src/services/envService.ts +++ b/frontend/src/services/envService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { ConfigMap, Secret } from '~/types'; export const getEnvSecret = (namespace: string, name: string): Promise => { diff --git a/frontend/src/services/groupSettingsService.ts b/frontend/src/services/groupSettingsService.ts index 2e957a342d..830d514add 100644 --- a/frontend/src/services/groupSettingsService.ts +++ b/frontend/src/services/groupSettingsService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { GroupsConfig } from '~/pages/groupSettings/groupTypes'; export const fetchGroupsSettings = (): Promise => { diff --git a/frontend/src/services/imagesService.ts b/frontend/src/services/imagesService.ts index e806227c24..1192b0f1ff 100644 --- a/frontend/src/services/imagesService.ts +++ b/frontend/src/services/imagesService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { BYONImage, ImageInfo, ResponseStatus } from '~/types'; export const fetchImages = (): Promise => { diff --git a/frontend/src/services/impersonateService.ts b/frontend/src/services/impersonateService.ts index 654087a705..d90a3659a3 100644 --- a/frontend/src/services/impersonateService.ts +++ b/frontend/src/services/impersonateService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; export const updateImpersonateSettings = (impersonate: boolean): Promise => { const url = '/api/dev-impersonate'; diff --git a/frontend/src/services/modelRegistrySettingsService.ts b/frontend/src/services/modelRegistrySettingsService.ts index a0671ada05..c77cf3b2f7 100644 --- a/frontend/src/services/modelRegistrySettingsService.ts +++ b/frontend/src/services/modelRegistrySettingsService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { ModelRegistryKind } from '~/k8sTypes'; import { RecursivePartial } from '~/typeHelpers'; diff --git a/frontend/src/services/notebookEventsService.ts b/frontend/src/services/notebookEventsService.ts index 52944f125c..f1b440fce9 100644 --- a/frontend/src/services/notebookEventsService.ts +++ b/frontend/src/services/notebookEventsService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { K8sEvent } from '~/types'; export const getNotebookEvents = ( diff --git a/frontend/src/services/notebookService.ts b/frontend/src/services/notebookService.ts index 73bad81027..eb43c35633 100644 --- a/frontend/src/services/notebookService.ts +++ b/frontend/src/services/notebookService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { RecursivePartial } from '~/typeHelpers'; import { Notebook, NotebookState, NotebookData, NotebookRunningState } from '~/types'; diff --git a/frontend/src/services/quickStartsService.ts b/frontend/src/services/quickStartsService.ts index d633c59fcc..50568c6f93 100644 --- a/frontend/src/services/quickStartsService.ts +++ b/frontend/src/services/quickStartsService.ts @@ -1,5 +1,5 @@ -import axios from 'axios'; import { QuickStart } from '@patternfly/quickstarts'; +import axios from '~/utilities/axios'; export const fetchQuickStarts = (): Promise => { const url = '/api/quickstarts'; diff --git a/frontend/src/services/roleBindingService.ts b/frontend/src/services/roleBindingService.ts index bbf264b9a9..b117139363 100644 --- a/frontend/src/services/roleBindingService.ts +++ b/frontend/src/services/roleBindingService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { RoleBinding } from '~/types'; export const getRoleBinding = (projectName: string, rbName: string): Promise => { diff --git a/frontend/src/services/routeService.ts b/frontend/src/services/routeService.ts index f7fd510a2d..f611b420e5 100644 --- a/frontend/src/services/routeService.ts +++ b/frontend/src/services/routeService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { Route } from '~/types'; export const getRoute = (namespace: string, routeName: string): Promise => { diff --git a/frontend/src/services/segmentKeyService.ts b/frontend/src/services/segmentKeyService.ts index 9b58fedc1e..2438936c9f 100644 --- a/frontend/src/services/segmentKeyService.ts +++ b/frontend/src/services/segmentKeyService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; import { ODHSegmentKey } from '~/types'; export const fetchSegmentKey = (): Promise => { diff --git a/frontend/src/services/storageService.ts b/frontend/src/services/storageService.ts index 690b205969..7327e8586d 100644 --- a/frontend/src/services/storageService.ts +++ b/frontend/src/services/storageService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; export const MAX_STORAGE_OBJECT_SIZE = 1e8; diff --git a/frontend/src/services/templateService.ts b/frontend/src/services/templateService.ts index 67e107b40c..88ddac572f 100644 --- a/frontend/src/services/templateService.ts +++ b/frontend/src/services/templateService.ts @@ -1,6 +1,6 @@ // TODO: Delete once we refactor Admin panel to support Passthrough API -import axios from 'axios'; import YAML from 'yaml'; +import axios from '~/utilities/axios'; import { assembleServingRuntimeTemplate } from '~/api'; import { ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; diff --git a/frontend/src/services/validateIsvService.ts b/frontend/src/services/validateIsvService.ts index e0a147320e..20c01d7254 100644 --- a/frontend/src/services/validateIsvService.ts +++ b/frontend/src/services/validateIsvService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '~/utilities/axios'; export const postValidateIsv = ( appName: string, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a3fcc4bcdd..7fe7e3a9a5 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -8,9 +8,20 @@ import { } from '@openshift/dynamic-plugin-sdk-utils'; import { AxiosError } from 'axios'; import { EnvironmentFromVariable } from '~/pages/projects/types'; -import { AcceleratorProfileKind, ImageStreamKind, ImageStreamSpecTagType } from './k8sTypes'; +import { + AcceleratorProfileKind, + DashboardCommonConfig, + ImageStreamKind, + ImageStreamSpecTagType, +} from './k8sTypes'; import { EitherNotBoth } from './typeHelpers'; +export type DevFeatureFlags = { + devFeatureFlags: Partial | null; + setDevFeatureFlag: (flag: keyof DashboardCommonConfig, value: boolean) => void; + resetDevFeatureFlags: () => void; +}; + export type PrometheusQueryResponse = { data: { result: ({ diff --git a/frontend/src/utilities/axios.ts b/frontend/src/utilities/axios.ts new file mode 100644 index 0000000000..a77f833cb0 --- /dev/null +++ b/frontend/src/utilities/axios.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line no-restricted-imports +import axios from 'axios'; + +export default axios.create(); diff --git a/frontend/src/utilities/useDetectUser.ts b/frontend/src/utilities/useDetectUser.ts index 0548f91f45..fda651e4ce 100644 --- a/frontend/src/utilities/useDetectUser.ts +++ b/frontend/src/utilities/useDetectUser.ts @@ -1,5 +1,5 @@ -import axios from 'axios'; import * as React from 'react'; +import axios from '~/utilities/axios'; import { getUserFulfilled, getUserPending, getUserRejected } from '~/redux/actions/actions'; import { useAppDispatch } from '~/redux/hooks'; import { POLL_INTERVAL } from './const';