From f81f56331686dea9867b5596ab29c9624c938eee Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Wed, 3 Jul 2024 12:35:20 -0400 Subject: [PATCH] [RHOAIENG-6982] About Modal in the Dashboard --- .../api/operator-subscription-status/index.ts | 26 +++ backend/src/types.ts | 6 + backend/src/utils/resourceUtils.ts | 2 + .../cypress/cypress/pages/aboutDialog.ts | 50 ++++++ .../cypress/cypress/support/commands/odh.ts | 5 + .../cypress/tests/mocked/application.cy.ts | 67 ++++++++ frontend/src/app/AboutDialog.scss | 9 + frontend/src/app/AboutDialog.tsx | 105 ++++++++++++ frontend/src/app/App.tsx | 1 + frontend/src/app/AppContext.ts | 1 + frontend/src/app/HeaderTools.tsx | 60 ++++--- .../src/app/__tests__/AboutDialog.spec.tsx | 156 ++++++++++++++++++ frontend/src/k8sTypes.ts | 8 + .../ManageInferenceServiceModal.spec.tsx | 1 + frontend/src/redux/selectors/index.ts | 1 + .../operatorSubscriptionStatusService.ts | 12 ++ frontend/src/types.ts | 7 + .../useWatchOperatorSubscriptionStatus.tsx | 6 + 18 files changed, 500 insertions(+), 23 deletions(-) create mode 100644 backend/src/routes/api/operator-subscription-status/index.ts create mode 100644 frontend/src/__tests__/cypress/cypress/pages/aboutDialog.ts create mode 100644 frontend/src/app/AboutDialog.scss create mode 100644 frontend/src/app/AboutDialog.tsx create mode 100644 frontend/src/app/__tests__/AboutDialog.spec.tsx create mode 100644 frontend/src/services/operatorSubscriptionStatusService.ts create mode 100644 frontend/src/utilities/useWatchOperatorSubscriptionStatus.tsx diff --git a/backend/src/routes/api/operator-subscription-status/index.ts b/backend/src/routes/api/operator-subscription-status/index.ts new file mode 100644 index 0000000000..6ca1d13e87 --- /dev/null +++ b/backend/src/routes/api/operator-subscription-status/index.ts @@ -0,0 +1,26 @@ +import { KubeFastifyInstance } from '../../../types'; +import { secureRoute } from '../../../utils/route-security'; +import { getSubscriptions, isRHOAI } from '../../../utils/resourceUtils'; +import { createCustomError } from '../../../utils/requestUtils'; + +module.exports = async (fastify: KubeFastifyInstance) => { + fastify.get( + '/', + secureRoute(fastify)(async () => { + const subscriptions = getSubscriptions(); + const subNamePrefix = isRHOAI(fastify) ? 'rhods-operator' : 'opendatahub-operator'; + const operatorSubscriptionStatus = subscriptions.find((sub) => + sub.installedCSV?.includes(subNamePrefix), + ); + if (operatorSubscriptionStatus) { + return operatorSubscriptionStatus; + } + fastify.log.error(`Failed to find operator subscription, ${subNamePrefix}`); + throw createCustomError( + 'Subscription unavailable', + 'Unable to get subscription information', + 404, + ); + }), + ); +}; diff --git a/backend/src/types.ts b/backend/src/types.ts index c786fa65b0..ab7f5a52c5 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -222,11 +222,15 @@ export type RouteKind = { // Minimal type for Subscriptions export type SubscriptionKind = { + spec: { + channel?: string; + }; status?: { installedCSV?: string; installPlanRef?: { namespace: string; }; + lastUpdated?: string; }; } & K8sResourceCommon; @@ -1019,8 +1023,10 @@ export type DataScienceClusterInitializationList = { }; export type SubscriptionStatusData = { + channel?: string; installedCSV?: string; installPlanRefNamespace?: string; + lastUpdated?: string; }; export type CronJobKind = { diff --git a/backend/src/utils/resourceUtils.ts b/backend/src/utils/resourceUtils.ts index 8d1a94b629..041beb1beb 100644 --- a/backend/src/utils/resourceUtils.ts +++ b/backend/src/utils/resourceUtils.ts @@ -148,8 +148,10 @@ const fetchSubscriptions = (fastify: KubeFastifyInstance): Promise ({ + channel: sub.spec.channel, installedCSV: sub.status?.installedCSV, installPlanRefNamespace: sub.status?.installPlanRef?.namespace, + lastUpdated: sub.status.lastUpdated, })); remainingItemCount = res.body?.metadata?.remainingItemCount; _continue = res.body?.metadata?.continue; diff --git a/frontend/src/__tests__/cypress/cypress/pages/aboutDialog.ts b/frontend/src/__tests__/cypress/cypress/pages/aboutDialog.ts new file mode 100644 index 0000000000..97517c0520 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/pages/aboutDialog.ts @@ -0,0 +1,50 @@ +import Chainable = Cypress.Chainable; + +export class AboutDialog { + show(wait = true): void { + cy.get('#help-icon-toggle').click(); + cy.findByTestId('help-about-item').click(); + if (wait) { + this.wait(); + } + } + + private wait() { + cy.findByTestId('home-page').should('be.visible'); + cy.testA11y(); + } + + findText(): Chainable> { + return cy.findByTestId('about-text'); + } + + findProductName(): Chainable> { + return cy.findByTestId('about-product-name'); + } + + findProductVersion(): Chainable> { + return cy.findByTestId('about-version'); + } + + findChannel(): Chainable> { + return cy.findByTestId('about-channel'); + } + + findAccessLevel(): Chainable> { + return cy.findByTestId('about-access-level'); + } + + findLastUpdate(): Chainable> { + return cy.findByTestId('about-last-update'); + } + + isAdminAccessLevel(): Chainable> { + return this.findAccessLevel().should('contain.text', 'Administrator'); + } + + isUserAccessLevel(): Chainable> { + return this.findAccessLevel().should('contain.text', 'Non-administrator'); + } +} + +export const aboutDialog = new AboutDialog(); diff --git a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts index 2ddc2e9c01..ac3a75d20b 100644 --- a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts +++ b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts @@ -32,6 +32,7 @@ import type { OdhDocument, PrometheusQueryRangeResponse, PrometheusQueryResponse, + SubscriptionStatusData, } from '~/types'; import type { ExperimentKFv2, @@ -111,6 +112,10 @@ declare global { type: 'GET /api/status', response: OdhResponse, ) => Cypress.Chainable) & + (( + type: 'GET /api/operator-subscription-status', + response: OdhResponse, + ) => Cypress.Chainable) & (( type: 'GET /api/status/openshift-ai-notebooks/allowedUsers', response: OdhResponse, diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/application.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/application.cy.ts index 7ca21277ee..fd454af321 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/application.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/application.cy.ts @@ -4,6 +4,8 @@ import { asProductAdminUser, asProjectAdminUser, } from '~/__tests__/cypress/cypress/utils/users'; +import { mockDashboardConfig } from '~/__mocks__'; +import { aboutDialog } from '~/__tests__/cypress/cypress/pages/aboutDialog'; describe('Application', () => { it('should disallow access to the dashboard', () => { @@ -35,4 +37,69 @@ describe('Application', () => { applicationLauncherMenuGroup.shouldHaveApplicationLauncherItem('OpenShift Cluster Manager'); applicationLauncher.toggleAppLauncherButton(); }); + + it('should show the about modal for ODH application', () => { + cy.interceptOdh('GET /api/operator-subscription-status', { + channel: 'fast', + lastUpdated: '2024-06-25T05:36:37Z', + }); + cy.interceptOdh('GET /api/dsci/status', { + conditions: [], + release: { + name: 'test application', + version: '1.0.1', + }, + }); + + appChrome.visit(); + aboutDialog.show(); + + aboutDialog.findText().should('contain.text', 'Open Data Hub'); + aboutDialog.findProductName().should('contain.text', 'test application'); + aboutDialog.findProductVersion().should('contain.text', '1.0.1'); + aboutDialog.findChannel().should('contain.text', 'fast'); + aboutDialog.isUserAccessLevel(); + aboutDialog.findLastUpdate().should('contain.text', 'June 25, 2024'); + }); + + it('should show the about modal correctly when release name is not available', () => { + cy.interceptOdh('GET /api/operator-subscription-status', { + channel: 'fast', + lastUpdated: '2024-06-25T05:36:37Z', + }); + // Handle no release name returned + cy.interceptOdh('GET /api/dsci/status', { + conditions: [], + release: { + version: '1.0.1', + }, + }); + appChrome.visit(); + aboutDialog.show(); + + aboutDialog.findProductName().should('contain.text', 'Open Data Hub'); + }); + + it('should show the about modal for RHOAI application', () => { + // Validate RHOAI about settings + const mockConfig = mockDashboardConfig({}); + mockConfig.metadata!.namespace = 'redhat-ods-applications'; + cy.interceptOdh('GET /api/config', mockConfig); + cy.interceptOdh('GET /api/operator-subscription-status', { + channel: 'fast', + lastUpdated: '2024-06-25T05:36:37Z', + }); + cy.interceptOdh('GET /api/dsci/status', { + conditions: [], + release: { + version: '1.0.1', + }, + }); + + appChrome.visit(); + aboutDialog.show(); + + aboutDialog.findText().should('contain.text', 'OpenShift'); + aboutDialog.findProductName().should('contain.text', 'OpenShift AI'); + }); }); diff --git a/frontend/src/app/AboutDialog.scss b/frontend/src/app/AboutDialog.scss new file mode 100644 index 0000000000..bc4a1a5627 --- /dev/null +++ b/frontend/src/app/AboutDialog.scss @@ -0,0 +1,9 @@ +// TODO: Remove when PF provides a fix for https://github.com/patternfly/patternfly/issues/6871 +// Override for about box height, reduce to fit the content instead of a fixed height +// Override to use the full width of the dialog rather than wrapping early +.odh-about-dialog { + height: fit-content; + .pf-v5-c-about-modal-box__content { + grid-column-end: -1; + } +} diff --git a/frontend/src/app/AboutDialog.tsx b/frontend/src/app/AboutDialog.tsx new file mode 100644 index 0000000000..566161c4b0 --- /dev/null +++ b/frontend/src/app/AboutDialog.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { + AboutModal, + Alert, + Bullseye, + Spinner, + TextContent, + TextList, + TextListItem, +} from '@patternfly/react-core'; +import { ODH_LOGO, ODH_PRODUCT_NAME } from '~/utilities/const'; +import { useUser, useClusterInfo } from '~/redux/selectors'; +import { useAppContext } from '~/app/AppContext'; +import useFetchDsciStatus from '~/concepts/areas/useFetchDsciStatus'; +import { useWatchOperatorSubscriptionStatus } from '~/utilities/useWatchOperatorSubscriptionStatus'; + +import './AboutDialog.scss'; + +const RhoaiAboutText = `Red Hat® OpenShift® AI (formerly Red Hat OpenShift Data Science) is a flexible, scalable MLOps platform for data scientists and developers of artificial intelligence and machine learning (AI/ML) applications. Built using open source technologies, OpenShift AI supports the full lifecycle of AI/ML experiments and models, on premise and in the public cloud.`; +const RhoaiDefaultReleaseName = `OpenShift AI`; + +const OdhAboutText = `Open Data Hub is an open source AI platform designed for the hybrid cloud. The community seeks to bridge the gap between application developers, data stewards, and data scientists by blending the leading open source AI tools with a unifying and intuitive user experience. Open Data Hub supports the full lifecycle of AI/ML experiments and models.`; +const OdhDefaultReleaseName = `Open Data Hub`; + +interface AboutDialogProps { + onClose: () => void; +} + +const AboutDialog: React.FC = ({ onClose }) => { + const { isAdmin } = useUser(); + const { isRHOAI } = useAppContext(); + const { serverURL } = useClusterInfo(); + const [dsciStatus, loadedDsci, errorDsci] = useFetchDsciStatus(); + const [subStatus, loadedSubStatus, errorSubStatus] = useWatchOperatorSubscriptionStatus(); + const error = errorDsci || errorSubStatus; + const loading = (!errorDsci && !loadedDsci) || (!errorSubStatus && !loadedSubStatus); + + return ( + + +

About

+

{isRHOAI ? RhoaiAboutText : OdhAboutText}

+
+ +
+ {loading ? ( + + + + ) : null} + + + {dsciStatus?.release?.name || + (isRHOAI ? RhoaiDefaultReleaseName : OdhDefaultReleaseName)}{' '} + version + + + {dsciStatus?.release?.version || 'Unknown'} + + Channel + + {subStatus?.channel || 'Unknown'} + + API server + + {serverURL} + + User type + + {isAdmin ? 'Administrator' : 'Non-administrator'} + + Last updated + + {subStatus?.lastUpdated + ? new Date(subStatus.lastUpdated).toLocaleString(undefined, { + dateStyle: 'long', + }) + : 'Unknown'} + + +
+ {error ? ( + + {error.message} + + ) : null} +
+
+ ); +}; + +export default AboutDialog; diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index 87721a836a..9bb6dcc9af 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -60,6 +60,7 @@ const App: React.FC = () => { buildStatuses, dashboardConfig, storageClasses, + isRHOAI: dashboardConfig.metadata?.namespace === 'redhat-ods-applications', } : null, [buildStatuses, dashboardConfig, storageClasses], diff --git a/frontend/src/app/AppContext.ts b/frontend/src/app/AppContext.ts index 2b51e99c15..5602e64b7d 100644 --- a/frontend/src/app/AppContext.ts +++ b/frontend/src/app/AppContext.ts @@ -6,6 +6,7 @@ type AppContextProps = { buildStatuses: BuildStatus[]; dashboardConfig: DashboardConfigKind; storageClasses: StorageClassKind[]; + isRHOAI: boolean; }; // eslint-disable-next-line @typescript-eslint/consistent-type-assertions diff --git a/frontend/src/app/HeaderTools.tsx b/frontend/src/app/HeaderTools.tsx index 2e4e413b9f..13e5b8ee7c 100644 --- a/frontend/src/app/HeaderTools.tsx +++ b/frontend/src/app/HeaderTools.tsx @@ -19,6 +19,7 @@ import useNotification from '~/utilities/useNotification'; import { updateImpersonateSettings } from '~/services/impersonateService'; import { AppNotification } from '~/redux/types'; import { useAppSelector } from '~/redux/hooks'; +import AboutDialog from '~/app/AboutDialog'; import AppLauncher from './AppLauncher'; import { useAppContext } from './AppContext'; import { logout } from './appUtils'; @@ -30,6 +31,7 @@ interface HeaderToolsProps { const HeaderTools: React.FC = ({ onNotificationsClick }) => { const [userMenuOpen, setUserMenuOpen] = React.useState(false); const [helpMenuOpen, setHelpMenuOpen] = React.useState(false); + const [aboutShown, setAboutShown] = React.useState(false); const notifications: AppNotification[] = useAppSelector((state) => state.notifications); const userName: string = useAppSelector((state) => state.user || ''); const isImpersonating: boolean = useAppSelector((state) => state.isImpersonating || false); @@ -116,6 +118,19 @@ const HeaderTools: React.FC = ({ onNotificationsClick }) => { ); } + helpMenuItems.push( + { + handleHelpClick(); + setAboutShown(true); + }} + > + About + , + ); + return ( @@ -133,29 +148,27 @@ const HeaderTools: React.FC = ({ onNotificationsClick }) => { onClick={onNotificationsClick} /> - {helpMenuItems.length > 0 ? ( - - setHelpMenuOpen(isOpen)} - toggle={(toggleRef) => ( - setHelpMenuOpen(!helpMenuOpen)} - isExpanded={helpMenuOpen} - > - - - )} - isOpen={helpMenuOpen} - > - {helpMenuItems} - - - ) : null} + + setHelpMenuOpen(isOpen)} + toggle={(toggleRef) => ( + setHelpMenuOpen(!helpMenuOpen)} + isExpanded={helpMenuOpen} + > + + + )} + isOpen={helpMenuOpen} + > + {helpMenuItems} + + {DEV_MODE && isImpersonating && ( @@ -197,6 +210,7 @@ const HeaderTools: React.FC = ({ onNotificationsClick }) => { + {aboutShown ? setAboutShown(false)} /> : null} ); }; diff --git a/frontend/src/app/__tests__/AboutDialog.spec.tsx b/frontend/src/app/__tests__/AboutDialog.spec.tsx new file mode 100644 index 0000000000..6bb5e92c93 --- /dev/null +++ b/frontend/src/app/__tests__/AboutDialog.spec.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { ClusterState, UserState } from '~/redux/selectors/types'; +import { useUser, useClusterInfo } from '~/redux/selectors'; +import { useAppContext } from '~/app/AppContext'; +import useFetchDsciStatus from '~/concepts/areas/useFetchDsciStatus'; +import { mockDashboardConfig } from '~/__mocks__'; +import { BuildStatus, SubscriptionStatusData } from '~/types'; +import { + DashboardConfigKind, + DataScienceClusterInitializationKindStatus, + StorageClassKind, +} from '~/k8sTypes'; +import { FetchState } from '~/utilities/useFetchState'; +import AboutDialog from '~/app/AboutDialog'; +import { useWatchOperatorSubscriptionStatus } from '~/utilities/useWatchOperatorSubscriptionStatus'; + +jest.mock('~/app/AppContext', () => ({ + __esModule: true, + useAppContext: jest.fn(), +})); + +jest.mock('~/redux/selectors', () => ({ + ...jest.requireActual('~/redux/selectors'), + useUser: jest.fn(), + useClusterInfo: jest.fn(), +})); + +jest.mock('~/concepts/areas/useFetchDsciStatus', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('~/utilities/useWatchOperatorSubscriptionStatus', () => ({ + __esModule: true, + useWatchOperatorSubscriptionStatus: jest.fn(), +})); + +const useUserMock = jest.mocked(useUser); +const useAppContextMock = jest.mocked(useAppContext); +const useClusterInfoMock = jest.mocked(useClusterInfo); +const useFetchDsciStatusMock = jest.mocked(useFetchDsciStatus); +const useWatchOperatorSubscriptionStatusMock = jest.mocked(useWatchOperatorSubscriptionStatus); + +describe('AboutDialog', () => { + let dashboardConfig: DashboardConfigKind; + let appContext: { + buildStatuses: BuildStatus[]; + dashboardConfig: DashboardConfigKind; + storageClasses: StorageClassKind[]; + isRHOAI: boolean; + }; + let userInfo: UserState; + const clusterInfo: ClusterState = { serverURL: 'https://test-server.com' }; + let dsciStatus: DataScienceClusterInitializationKindStatus; + let dsciFetchStatus: FetchState; + let operatorSubscriptionStatus: SubscriptionStatusData; + let operatorSubscriptionFetchStatus: FetchState; + + beforeEach(() => { + dashboardConfig = mockDashboardConfig({}); + dashboardConfig.metadata!.namespace = 'odh-dashboard'; + appContext = { + buildStatuses: [], + dashboardConfig, + storageClasses: [], + isRHOAI: false, + }; + dsciStatus = { + conditions: [], + release: { + // eslint-disable-next-line no-restricted-syntax + name: 'Open Data Hub Operator', + version: '1.0.1', + }, + }; + dsciFetchStatus = [dsciStatus, true, undefined, () => Promise.resolve(dsciStatus)]; + userInfo = { + username: 'test-user', + isAdmin: false, + isAllowed: true, + userLoading: false, + userError: null, + }; + operatorSubscriptionStatus = { + channel: 'fast', + lastUpdated: '2024-06-25T05:36:37Z', + }; + operatorSubscriptionFetchStatus = [ + operatorSubscriptionStatus, + true, + undefined, + () => Promise.resolve(operatorSubscriptionStatus), + ]; + }); + + it('should show the appropriate odh values', async () => { + useAppContextMock.mockReturnValue(appContext); + useUserMock.mockReturnValue(userInfo); + useClusterInfoMock.mockReturnValue(clusterInfo); + useFetchDsciStatusMock.mockReturnValue(dsciFetchStatus); + useWatchOperatorSubscriptionStatusMock.mockReturnValue(operatorSubscriptionFetchStatus); + + // eslint-disable-next-line no-restricted-syntax + render(); + + const aboutText = await screen.findByTestId('about-text'); + const name = await screen.findByTestId('about-product-name'); + const version = await screen.findByTestId('about-version'); + const channel = await screen.findByTestId('about-channel'); + const accessLevel = await screen.findByTestId('about-access-level'); + const lastUpdate = await screen.findByTestId('about-last-update'); + + // eslint-disable-next-line no-restricted-syntax + expect(aboutText.textContent).toContain('Open Data Hub'); + // eslint-disable-next-line no-restricted-syntax + expect(name.textContent).toContain('Open Data Hub'); + expect(version.textContent).toContain('1.0.1'); + expect(channel.textContent).toContain('fast'); + expect(accessLevel.textContent).toContain('Non-administrator'); + expect(lastUpdate.textContent).toContain('June 25, 2024'); + }); + + it('should show the appropriate RHOAI values', async () => { + dashboardConfig.metadata!.namespace = 'redhat-ods-applications'; + appContext.isRHOAI = true; + userInfo.isAdmin = true; + dsciStatus.release!.name = 'OpenShift AI Self-Managed version'; + + useAppContextMock.mockReturnValue(appContext); + useUserMock.mockReturnValue(userInfo); + useClusterInfoMock.mockReturnValue(clusterInfo); + useFetchDsciStatusMock.mockReturnValue(dsciFetchStatus); + useWatchOperatorSubscriptionStatusMock.mockReturnValue(operatorSubscriptionFetchStatus); + + // eslint-disable-next-line no-restricted-syntax + render(); + + const aboutText = await screen.findByTestId('about-text'); + const name = await screen.findByTestId('about-product-name'); + const version = await screen.findByTestId('about-version'); + const channel = await screen.findByTestId('about-channel'); + const accessLevel = await screen.findByTestId('about-access-level'); + const lastUpdate = await screen.findByTestId('about-last-update'); + + // eslint-disable-next-line no-restricted-syntax + expect(aboutText.textContent).toContain('OpenShift'); + expect(name.textContent).toContain('OpenShift AI'); + expect(version.textContent).toContain('1.0.1'); + expect(channel.textContent).toContain('fast'); + expect(accessLevel.textContent).toContain('Administrator'); + expect(lastUpdate.textContent).toContain('June 25, 2024'); + }); +}); diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 58714030f4..90068c4393 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -1283,10 +1283,18 @@ export type DataScienceClusterKindStatus = { conditions: K8sCondition[]; installedComponents: { [key in StackComponent]?: boolean }; phase?: string; + release?: { + name: string; + version: string; + }; }; export type DataScienceClusterInitializationKindStatus = { conditions: K8sCondition[]; + release?: { + name?: string; + version?: string; + }; phase?: string; }; diff --git a/frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/__tests__/ManageInferenceServiceModal.spec.tsx b/frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/__tests__/ManageInferenceServiceModal.spec.tsx index 771f946a38..2063873c85 100644 --- a/frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/__tests__/ManageInferenceServiceModal.spec.tsx +++ b/frontend/src/pages/modelServing/screens/projects/InferenceServiceModal/__tests__/ManageInferenceServiceModal.spec.tsx @@ -36,6 +36,7 @@ describe('ManageInferenceServiceModal', () => { buildStatuses: [], dashboardConfig: mockDashboardConfig({}), storageClasses: [], + isRHOAI: false, }); const currentProject = mockProjectK8sResource({}); diff --git a/frontend/src/redux/selectors/index.ts b/frontend/src/redux/selectors/index.ts index 5c4daedfb3..a563b80c9e 100644 --- a/frontend/src/redux/selectors/index.ts +++ b/frontend/src/redux/selectors/index.ts @@ -1,3 +1,4 @@ export * from './project'; export * from './types'; export * from './user'; +export * from './clusterInfo'; diff --git a/frontend/src/services/operatorSubscriptionStatusService.ts b/frontend/src/services/operatorSubscriptionStatusService.ts new file mode 100644 index 0000000000..3073cabc94 --- /dev/null +++ b/frontend/src/services/operatorSubscriptionStatusService.ts @@ -0,0 +1,12 @@ +import axios from 'axios'; +import { SubscriptionStatusData } from '~/types'; + +export const fetchOperatorSubscriptionStatus = (): Promise => { + const url = '/api/operator-subscription-status'; + return axios + .get(url) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 3dc0e9caf6..962d895880 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -236,6 +236,13 @@ export type BuildStatus = { timestamp: string; }; +export type SubscriptionStatusData = { + channel?: string; + installedCSV?: string; + installPlanRefNamespace?: string; + lastUpdated?: string; +}; + type K8sMetadata = { name: string; namespace?: string; diff --git a/frontend/src/utilities/useWatchOperatorSubscriptionStatus.tsx b/frontend/src/utilities/useWatchOperatorSubscriptionStatus.tsx new file mode 100644 index 0000000000..40d2362357 --- /dev/null +++ b/frontend/src/utilities/useWatchOperatorSubscriptionStatus.tsx @@ -0,0 +1,6 @@ +import { fetchOperatorSubscriptionStatus } from '~/services/operatorSubscriptionStatusService'; +import { SubscriptionStatusData } from '~/types'; +import useFetchState, { FetchState } from '~/utilities/useFetchState'; + +export const useWatchOperatorSubscriptionStatus = (): FetchState => + useFetchState(fetchOperatorSubscriptionStatus, null);