From a6d19aef34ed36b7c3d56fe4209c8464da8d0e8c Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Thu, 18 Apr 2024 08:48:51 -0400 Subject: [PATCH] [RHOAIENG-5491] Landing page: Info section about AI flows --- .../cypress/cypress/e2e/home/home.cy.ts | 106 ++++++++++++++++-- .../src/concepts/design/DividedGallery.tsx | 14 ++- .../src/concepts/design/InfoGalleryItem.tsx | 6 +- .../src/concepts/design/TypeBorderCard.scss | 63 ++++++++++- .../src/concepts/design/TypeBorderedCard.tsx | 18 ++- frontend/src/concepts/design/vars.scss | 4 +- .../images/UI_icon-Red_Hat-Branch-Color.svg | 4 + .../images/UI_icon-Red_Hat-Chart-Color.svg | 6 + .../images/UI_icon-Red_Hat-Folder-Color.svg | 4 + frontend/src/pages/home/Home.tsx | 37 +++--- .../src/pages/home/aiFlows/AIFlowCard.tsx | 56 +++++++++ .../home/aiFlows/CreateAndTrainGallery.tsx | 61 ++++++++++ .../home/aiFlows/DeployAndMonitorGallery.tsx | 49 ++++++++ .../src/pages/home/aiFlows/InfoGallery.tsx | 29 +++++ .../pages/home/aiFlows/ProjectsGallery.tsx | 60 ++++++++++ .../src/pages/home/aiFlows/useAIFlows.tsx | 106 ++++++++++++++++++ 16 files changed, 593 insertions(+), 30 deletions(-) create mode 100644 frontend/src/images/UI_icon-Red_Hat-Branch-Color.svg create mode 100644 frontend/src/images/UI_icon-Red_Hat-Chart-Color.svg create mode 100644 frontend/src/images/UI_icon-Red_Hat-Folder-Color.svg create mode 100644 frontend/src/pages/home/aiFlows/AIFlowCard.tsx create mode 100644 frontend/src/pages/home/aiFlows/CreateAndTrainGallery.tsx create mode 100644 frontend/src/pages/home/aiFlows/DeployAndMonitorGallery.tsx create mode 100644 frontend/src/pages/home/aiFlows/InfoGallery.tsx create mode 100644 frontend/src/pages/home/aiFlows/ProjectsGallery.tsx create mode 100644 frontend/src/pages/home/aiFlows/useAIFlows.tsx diff --git a/frontend/src/__tests__/cypress/cypress/e2e/home/home.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/home/home.cy.ts index 847948fc5a..6e23f59681 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/home/home.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/home/home.cy.ts @@ -4,21 +4,25 @@ import { enabledPage } from '~/__tests__/cypress/cypress/pages/enabled'; type HandlersProps = { disableHome?: boolean; + disableProjects?: boolean; + disableModelServing?: boolean; + disablePipelines?: boolean; }; -const initIntercepts = ({ disableHome }: HandlersProps) => { - cy.interceptOdh( - 'GET /api/config', - mockDashboardConfig({ - disableHome, - }), - ); +const initIntercepts = (config: HandlersProps = {}) => { + const dashboardConfig = { + disableProjects: false, + disableModelServing: false, + disablePipelines: false, + ...config, + }; + cy.interceptOdh('GET /api/config', mockDashboardConfig(dashboardConfig)); cy.interceptOdh('GET /api/components', { query: { installed: 'true' } }, mockComponents()); }; describe('Home page', () => { it('should not be shown by default', () => { - initIntercepts({}); + initIntercepts(); cy.visit('/'); cy.findByTestId('enabled-application').should('be.visible'); }); @@ -30,4 +34,90 @@ describe('Home page', () => { // enabled applications page is still navigable enabledPage.visit(true); }); + it('should show the appropriate AI flow cards', () => { + initIntercepts({ disableHome: false }); + cy.visit('/'); + + cy.findByTestId('ai-flow-projects-card').should('be.visible'); + cy.findByTestId('ai-flow-train-card').should('be.visible'); + cy.findByTestId('ai-flow-models-card').should('be.visible'); + }); + it('should show the appropriate info cards', () => { + initIntercepts({ disableHome: false }); + cy.visit('/'); + + cy.findByTestId('ai-flow-projects-card').click(); + cy.findByTestId('ai-flows-projects-info').should('be.visible'); + cy.findByTestId('ai-flows-connections-info').should('be.visible'); + cy.findByTestId('ai-flows-storage-info').should('be.visible'); + + cy.findByTestId('ai-flow-train-card').click(); + cy.findByTestId('ai-flows-workbenches-info').should('be.visible'); + cy.findByTestId('ai-flows-pipelines-info').should('be.visible'); + cy.findByTestId('ai-flows-runs-info').should('be.visible'); + + cy.findByTestId('ai-flow-models-card').click(); + cy.findByTestId('ai-flows-model-servers-info').should('be.visible'); + cy.findByTestId('ai-flows-model-deploy-info').should('be.visible'); + }); + it('should close the info cards on re-click', () => { + initIntercepts({ disableHome: false }); + cy.visit('/'); + + cy.findByTestId('ai-flow-projects-card').click(); + cy.findByTestId('ai-flows-projects-info').should('be.visible'); + cy.findByTestId('ai-flows-connections-info').should('be.visible'); + cy.findByTestId('ai-flows-storage-info').should('be.visible'); + + cy.findByTestId('ai-flow-projects-card').click(); + cy.findByTestId('ai-flows-projects-info').should('not.exist'); + cy.findByTestId('ai-flows-connections-info').should('not.exist'); + cy.findByTestId('ai-flows-storage-info').should('not.exist'); + }); + it('should close the info cards on close button click', () => { + initIntercepts({ disableHome: false }); + cy.visit('/'); + + cy.findByTestId('ai-flow-projects-card').click(); + cy.findByTestId('ai-flows-projects-info').should('be.visible'); + cy.findByTestId('ai-flows-connections-info').should('be.visible'); + cy.findByTestId('ai-flows-storage-info').should('be.visible'); + + cy.findByTestId('ai-flows-close-info').click(); + cy.findByTestId('ai-flows-projects-info').should('not.exist'); + cy.findByTestId('ai-flows-connections-info').should('not.exist'); + cy.findByTestId('ai-flows-storage-info').should('not.exist'); + }); + it('should hide sections that are disabled', () => { + initIntercepts({ + disableHome: false, + disableProjects: true, + }); + cy.visit('/'); + cy.findByTestId('home-page').should('be.visible'); + + cy.findByTestId('ai-flow-projects-card').should('not.exist'); + + initIntercepts({ + disableHome: false, + disableModelServing: true, + }); + cy.visit('/'); + cy.findByTestId('home-page').should('be.visible'); + cy.findByTestId('ai-flow-models-card').should('not.exist'); + }); + it('should hide info cards that are disabled', () => { + initIntercepts({ + disableHome: false, + disablePipelines: true, + }); + cy.visit('/'); + cy.findByTestId('home-page').should('be.visible'); + + cy.findByTestId('ai-flow-train-card').click(); + + cy.findByTestId('ai-flows-workbenches-info').should('be.visible'); + cy.findByTestId('ai-flows-pipelines-info').should('not.exist'); + cy.findByTestId('ai-flows-runs-info').should('not.exist'); + }); }); diff --git a/frontend/src/concepts/design/DividedGallery.tsx b/frontend/src/concepts/design/DividedGallery.tsx index db84c8101b..4f084790a1 100644 --- a/frontend/src/concepts/design/DividedGallery.tsx +++ b/frontend/src/concepts/design/DividedGallery.tsx @@ -7,7 +7,9 @@ type DividedGalleryProps = Omit & { minSize: string; itemCount: number; showClose?: boolean; + closeAlt?: string; onClose?: () => void; + closeTestId?: string; }; import './DividedGallery.scss'; @@ -16,9 +18,11 @@ const DividedGallery: React.FC = ({ minSize, itemCount, showClose, + closeAlt, onClose, children, className, + closeTestId, ...rest }) => (
@@ -30,8 +34,14 @@ const DividedGallery: React.FC = ({ {children} {showClose ? (
-
) : null} diff --git a/frontend/src/concepts/design/InfoGalleryItem.tsx b/frontend/src/concepts/design/InfoGalleryItem.tsx index 690e58916a..bfff5b0c23 100644 --- a/frontend/src/concepts/design/InfoGalleryItem.tsx +++ b/frontend/src/concepts/design/InfoGalleryItem.tsx @@ -4,6 +4,7 @@ import { ButtonVariant, Flex, FlexItem, + GalleryItemProps, Stack, StackItem, Text, @@ -22,7 +23,7 @@ type InfoGalleryItemProps = { description: string; isOpen: boolean; onClick?: () => void; -}; +} & GalleryItemProps; const InfoGalleryItem: React.FC = ({ title, @@ -31,8 +32,9 @@ const InfoGalleryItem: React.FC = ({ description, isOpen, onClick, + ...rest }) => ( - + = ({ objectType, sectionType, className, + selectable, + selected, ...rest }) => ( - + ); export default TypeBorderedCard; diff --git a/frontend/src/concepts/design/vars.scss b/frontend/src/concepts/design/vars.scss index 8464561ffb..bfff386084 100644 --- a/frontend/src/concepts/design/vars.scss +++ b/frontend/src/concepts/design/vars.scss @@ -1,11 +1,11 @@ :root { --ai-set-up--BackgroundColor: #ffe8cc; - --ai-organize--BackgroundColor: #fff4cc; + --ai-organize--BackgroundColor: #ffe8cc; --ai-training--BackgroundColor: #daf2f2; --ai-serving--BackgroundColor: #e0f0ff; --ai-set-up--BorderColor: #f8ae54; - --ai-organize--BorderColor: #ffcc17; + --ai-organize--BorderColor: #f8ae54; --ai-training--BorderColor: #9ad8d8; --ai-serving--BorderColor: #92c5f9; diff --git a/frontend/src/images/UI_icon-Red_Hat-Branch-Color.svg b/frontend/src/images/UI_icon-Red_Hat-Branch-Color.svg new file mode 100644 index 0000000000..b5c4c99de5 --- /dev/null +++ b/frontend/src/images/UI_icon-Red_Hat-Branch-Color.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/images/UI_icon-Red_Hat-Chart-Color.svg b/frontend/src/images/UI_icon-Red_Hat-Chart-Color.svg new file mode 100644 index 0000000000..0ea53d0830 --- /dev/null +++ b/frontend/src/images/UI_icon-Red_Hat-Chart-Color.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/images/UI_icon-Red_Hat-Folder-Color.svg b/frontend/src/images/UI_icon-Red_Hat-Folder-Color.svg new file mode 100644 index 0000000000..01e3e0f95b --- /dev/null +++ b/frontend/src/images/UI_icon-Red_Hat-Folder-Color.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index 044e2d1a38..20c12de706 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -8,20 +8,29 @@ import { PageSectionVariants, } from '@patternfly/react-core'; import { HomeIcon } from '@patternfly/react-icons'; +import { ODH_PRODUCT_NAME } from '~/utilities/const'; +import { useAIFlows } from './aiFlows/useAIFlows'; -const Home: React.FC = () => ( - - - - } - alt="" - /> - - - -); +const Home: React.FC = () => { + const aiFlows = useAIFlows(); + + if (!aiFlows) { + return ( + + + + } + /> + + + + ); + } + + return
{aiFlows}
; +}; export default Home; diff --git a/frontend/src/pages/home/aiFlows/AIFlowCard.tsx b/frontend/src/pages/home/aiFlows/AIFlowCard.tsx new file mode 100644 index 0000000000..7940efe564 --- /dev/null +++ b/frontend/src/pages/home/aiFlows/AIFlowCard.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { Bullseye, CardBody, CardProps, Stack, Text, TextContent } from '@patternfly/react-core'; +import TypeBorderedCard from '~/concepts/design/TypeBorderedCard'; +import { SectionType } from '~/concepts/design/utils'; + +type AIFlowCardProps = { + title: string; + imgSrc: string; + imgAlt: string; + sectionType: SectionType; + selected: boolean; + onSelect: () => void; +} & CardProps; + +const AIFlowCard: React.FC = ({ + title, + imgSrc, + imgAlt, + sectionType, + selected, + onSelect, + ...rest +}) => ( + onSelect()} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + event.stopPropagation(); + onSelect(); + } + }} + tabIndex={0} + {...rest} + > + + + + {imgAlt} + + + + + {title} + + + + + + +); + +export default AIFlowCard; diff --git a/frontend/src/pages/home/aiFlows/CreateAndTrainGallery.tsx b/frontend/src/pages/home/aiFlows/CreateAndTrainGallery.tsx new file mode 100644 index 0000000000..020fbf7d37 --- /dev/null +++ b/frontend/src/pages/home/aiFlows/CreateAndTrainGallery.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { ProjectObjectType, SectionType, typedObjectImage } from '~/concepts/design/utils'; +import InfoGalleryItem from '~/concepts/design/InfoGalleryItem'; +import { SupportedArea } from '~/concepts/areas'; +import useIsAreaAvailable from '~/concepts/areas/useIsAreaAvailable'; +import InfoGallery from './InfoGallery'; + +const CreateAndTrainGallery: React.FC<{ onClose: () => void }> = ({ onClose }) => { + const { status: workbenchesAvailable } = useIsAreaAvailable(SupportedArea.WORKBENCHES); + const { status: pipelinesAvailable } = useIsAreaAvailable(SupportedArea.DS_PIPELINES); + + const infoItems = []; + + if (workbenchesAvailable) { + infoItems.push( + , + ); + } + + if (pipelinesAvailable) { + infoItems.push( + , + , + ); + } + + return ( + + ); +}; + +export default CreateAndTrainGallery; diff --git a/frontend/src/pages/home/aiFlows/DeployAndMonitorGallery.tsx b/frontend/src/pages/home/aiFlows/DeployAndMonitorGallery.tsx new file mode 100644 index 0000000000..2a09f03598 --- /dev/null +++ b/frontend/src/pages/home/aiFlows/DeployAndMonitorGallery.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { ProjectObjectType, SectionType, typedObjectImage } from '~/concepts/design/utils'; +import InfoGalleryItem from '~/concepts/design/InfoGalleryItem'; +import useServingPlatformStatuses from '~/pages/modelServing/useServingPlatformStatuses'; +import InfoGallery from './InfoGallery'; + +const DeployAndMonitorGallery: React.FC<{ onClose: () => void }> = ({ onClose }) => { + const servingPlatformStatuses = useServingPlatformStatuses(); + const modelMeshEnabled = servingPlatformStatuses.modelMesh.enabled; + + const infoItems = []; + + if (modelMeshEnabled) { + infoItems.push( + , + ); + } + + infoItems.push( + , + ); + + return ( + + ); +}; + +export default DeployAndMonitorGallery; diff --git a/frontend/src/pages/home/aiFlows/InfoGallery.tsx b/frontend/src/pages/home/aiFlows/InfoGallery.tsx new file mode 100644 index 0000000000..4b0c095d3d --- /dev/null +++ b/frontend/src/pages/home/aiFlows/InfoGallery.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import DividedGallery from '~/concepts/design/DividedGallery'; + +type InfoGalleryProps = { + infoItems: React.ReactNode[]; + closeAlt: string; + onClose: () => void; + closeTestId: string; +}; + +const InfoGallery: React.FC = ({ infoItems, closeAlt, onClose, closeTestId }) => + infoItems.length > 0 ? ( + + {...infoItems} + + ) : null; + +export default InfoGallery; diff --git a/frontend/src/pages/home/aiFlows/ProjectsGallery.tsx b/frontend/src/pages/home/aiFlows/ProjectsGallery.tsx new file mode 100644 index 0000000000..1ce047c019 --- /dev/null +++ b/frontend/src/pages/home/aiFlows/ProjectsGallery.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { ProjectObjectType, SectionType, typedObjectImage } from '~/concepts/design/utils'; +import InfoGalleryItem from '~/concepts/design/InfoGalleryItem'; +import { SupportedArea } from '~/concepts/areas'; +import useIsAreaAvailable from '~/concepts/areas/useIsAreaAvailable'; +import useServingPlatformStatuses from '~/pages/modelServing/useServingPlatformStatuses'; +import InfoGallery from './InfoGallery'; + +const ProjectsGallery: React.FC<{ onClose: () => void }> = ({ onClose }) => { + const { status: pipelinesAvailable } = useIsAreaAvailable(SupportedArea.DS_PIPELINES); + const servingPlatformStatuses = useServingPlatformStatuses(); + const modelMeshEnabled = servingPlatformStatuses.modelMesh.enabled; + + const projectDescription = `Projects allow you to organize your related work in one place. You can add workbenches, ${ + pipelinesAvailable ? 'pipelines, ' : '' + }cluster storage, data connections, and model${ + modelMeshEnabled ? ' servers' : 's' + } to your project.`; + + const infoItems = [ + , + , + , + ]; + + return ( + + ); +}; + +export default ProjectsGallery; diff --git a/frontend/src/pages/home/aiFlows/useAIFlows.tsx b/frontend/src/pages/home/aiFlows/useAIFlows.tsx new file mode 100644 index 0000000000..3a5533836c --- /dev/null +++ b/frontend/src/pages/home/aiFlows/useAIFlows.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { Gallery, PageSection, Stack, Text, TextContent } from '@patternfly/react-core'; +import projectIcon from '~/images/UI_icon-Red_Hat-Folder-Color.svg'; +import pipelineIcon from '~/images/UI_icon-Red_Hat-Branch-Color.svg'; +import chartIcon from '~/images/UI_icon-Red_Hat-Chart-Color.svg'; +import { SectionType } from '~/concepts/design/utils'; +import useIsAreaAvailable from '~/concepts/areas/useIsAreaAvailable'; +import { SupportedArea } from '~/concepts/areas'; +import ProjectsGallery from './ProjectsGallery'; +import CreateAndTrainGallery from './CreateAndTrainGallery'; +import DeployAndMonitorGallery from './DeployAndMonitorGallery'; +import AIFlowCard from './AIFlowCard'; + +export const useAIFlows = (): React.ReactNode => { + const { status: workbenchesAvailable } = useIsAreaAvailable(SupportedArea.WORKBENCHES); + const { status: pipelinesAvailable } = useIsAreaAvailable(SupportedArea.DS_PIPELINES); + const { status: projectsAvailable } = useIsAreaAvailable(SupportedArea.DS_PROJECTS_VIEW); + const { status: modelServingAvailable } = useIsAreaAvailable(SupportedArea.MODEL_SERVING); + const [selected, setSelected] = React.useState(); + + return React.useMemo(() => { + const cards = []; + if (projectsAvailable) { + cards.push( + setSelected((prev) => (prev === 'organize' ? undefined : 'organize'))} + />, + ); + } + if (workbenchesAvailable || pipelinesAvailable) { + cards.push( + setSelected((prev) => (prev === 'train' ? undefined : 'train'))} + />, + ); + } + if (modelServingAvailable) { + cards.push( + setSelected((prev) => (prev === 'serving' ? undefined : 'serving'))} + />, + ); + } + + if (!cards.length) { + return null; + } + + const galleryWidths = `calc(${100 / cards.length}% - (1rem * ${cards.length - 1} / ${ + cards.length + }))`; + + return ( + + + + Train, serve, monitor, and manage AI/ML models + + + {cards} + + {selected === 'organize' ? ( + setSelected(undefined)} /> + ) : null} + {selected === 'train' ? ( + setSelected(undefined)} /> + ) : null} + {selected === 'serving' ? ( + setSelected(undefined)} /> + ) : null} + + + ); + }, [ + modelServingAvailable, + pipelinesAvailable, + projectsAvailable, + selected, + workbenchesAvailable, + ]); +};