Skip to content

Commit

Permalink
Merge pull request #2984 from jeff-phillips-18/about-dialog
Browse files Browse the repository at this point in the history
[RHOAIENG-6982] About Modal in the Dashboard
  • Loading branch information
openshift-merge-bot[bot] authored Jul 12, 2024
2 parents ee4410c + f81f563 commit cb6e509
Show file tree
Hide file tree
Showing 18 changed files with 500 additions and 23 deletions.
26 changes: 26 additions & 0 deletions backend/src/routes/api/operator-subscription-status/index.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}),
);
};
6 changes: 6 additions & 0 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -1019,8 +1023,10 @@ export type DataScienceClusterInitializationList = {
};

export type SubscriptionStatusData = {
channel?: string;
installedCSV?: string;
installPlanRefNamespace?: string;
lastUpdated?: string;
};

export type CronJobKind = {
Expand Down
2 changes: 2 additions & 0 deletions backend/src/utils/resourceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,10 @@ const fetchSubscriptions = (fastify: KubeFastifyInstance): Promise<SubscriptionS
};
};
const subs = res?.body.items?.map((sub) => ({
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;
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/__tests__/cypress/cypress/pages/aboutDialog.ts
Original file line number Diff line number Diff line change
@@ -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<JQuery<HTMLElement>> {
return cy.findByTestId('about-text');
}

findProductName(): Chainable<JQuery<HTMLElement>> {
return cy.findByTestId('about-product-name');
}

findProductVersion(): Chainable<JQuery<HTMLElement>> {
return cy.findByTestId('about-version');
}

findChannel(): Chainable<JQuery<HTMLElement>> {
return cy.findByTestId('about-channel');
}

findAccessLevel(): Chainable<JQuery<HTMLElement>> {
return cy.findByTestId('about-access-level');
}

findLastUpdate(): Chainable<JQuery<HTMLElement>> {
return cy.findByTestId('about-last-update');
}

isAdminAccessLevel(): Chainable<JQuery<HTMLElement>> {
return this.findAccessLevel().should('contain.text', 'Administrator');
}

isUserAccessLevel(): Chainable<JQuery<HTMLElement>> {
return this.findAccessLevel().should('contain.text', 'Non-administrator');
}
}

export const aboutDialog = new AboutDialog();
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
OdhDocument,
PrometheusQueryRangeResponse,
PrometheusQueryResponse,
SubscriptionStatusData,
} from '~/types';
import type {
ExperimentKFv2,
Expand Down Expand Up @@ -111,6 +112,10 @@ declare global {
type: 'GET /api/status',
response: OdhResponse<StatusResponse>,
) => Cypress.Chainable<null>) &
((
type: 'GET /api/operator-subscription-status',
response: OdhResponse<SubscriptionStatusData>,
) => Cypress.Chainable<null>) &
((
type: 'GET /api/status/openshift-ai-notebooks/allowedUsers',
response: OdhResponse<AllowedUser[]>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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');
});
});
9 changes: 9 additions & 0 deletions frontend/src/app/AboutDialog.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
105 changes: 105 additions & 0 deletions frontend/src/app/AboutDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<AboutDialogProps> = ({ 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 (
<AboutModal
className="odh-about-dialog"
isOpen
onClose={onClose}
brandImageSrc={`${window.location.origin}/images/${ODH_LOGO}`}
brandImageAlt={`${ODH_PRODUCT_NAME} logo`}
productName={ODH_PRODUCT_NAME}
aria-label={ODH_PRODUCT_NAME}
data-testid="odh-about-dialog"
>
<TextContent style={{ paddingBottom: 48 }}>
<h4>About</h4>
<p data-testid="about-text">{isRHOAI ? RhoaiAboutText : OdhAboutText}</p>
</TextContent>
<TextContent>
<div style={{ position: 'relative' }}>
{loading ? (
<Bullseye style={{ position: 'absolute', width: '100%' }}>
<Spinner size="xl" />
</Bullseye>
) : null}
<TextList component="dl" style={{ visibility: loading ? 'hidden' : 'visible' }}>
<TextListItem component="dt" data-testid="about-product-name">
{dsciStatus?.release?.name ||
(isRHOAI ? RhoaiDefaultReleaseName : OdhDefaultReleaseName)}{' '}
version
</TextListItem>
<TextListItem component="dd" data-testid="about-version">
{dsciStatus?.release?.version || 'Unknown'}
</TextListItem>
<TextListItem component="dt">Channel</TextListItem>
<TextListItem component="dd" data-testid="about-channel">
{subStatus?.channel || 'Unknown'}
</TextListItem>
<TextListItem component="dt">API server</TextListItem>
<TextListItem component="dd" data-testid="about-server-url">
{serverURL}
</TextListItem>
<TextListItem component="dt">User type</TextListItem>
<TextListItem component="dd" data-testid="about-access-level">
{isAdmin ? 'Administrator' : 'Non-administrator'}
</TextListItem>
<TextListItem component="dt">Last updated</TextListItem>
<TextListItem component="dd" data-testid="about-last-update">
{subStatus?.lastUpdated
? new Date(subStatus.lastUpdated).toLocaleString(undefined, {
dateStyle: 'long',
})
: 'Unknown'}
</TextListItem>
</TextList>
</div>
{error ? (
<Alert
style={{ marginTop: 'var(--pf-v5-global--spacer--lg)' }}
variant="danger"
title="Problem loading product information"
>
{error.message}
</Alert>
) : null}
</TextContent>
</AboutModal>
);
};

export default AboutDialog;
1 change: 1 addition & 0 deletions frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const App: React.FC = () => {
buildStatuses,
dashboardConfig,
storageClasses,
isRHOAI: dashboardConfig.metadata?.namespace === 'redhat-ods-applications',
}
: null,
[buildStatuses, dashboardConfig, storageClasses],
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/AppContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type AppContextProps = {
buildStatuses: BuildStatus[];
dashboardConfig: DashboardConfigKind;
storageClasses: StorageClassKind[];
isRHOAI: boolean;
};

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
Expand Down
Loading

0 comments on commit cb6e509

Please sign in to comment.