Skip to content

Commit

Permalink
[RHOAIENG-5489] Home page: projects w/ tiles
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 committed Apr 24, 2024
1 parent d155ef5 commit 2ae6e4e
Show file tree
Hide file tree
Showing 11 changed files with 464 additions and 45 deletions.
95 changes: 93 additions & 2 deletions frontend/src/__tests__/cypress/cypress/e2e/home/homeProjects.cy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { initHomeIntercepts } from '~/__tests__/cypress/cypress/e2e/home/homeUtils';
import { mockSelfSubjectAccessReview } from '~/__mocks__/mockSelfSubjectAccessReview';
import { createProjectModal } from '~/__tests__/cypress/cypress/pages/projects';
import { SelfSubjectAccessReviewModel } from '~/__tests__/cypress/cypress/utils/models';
import {
createProjectModal,
projectDetails,
projectListPage,
} from '~/__tests__/cypress/cypress/pages/projects';
import {
ProjectModel,
SelfSubjectAccessReviewModel,
} from '~/__tests__/cypress/cypress/utils/models';
import { mockProjectsK8sList } from '~/__mocks__';

const interceptAccessReview = (allowed: boolean) => {
cy.interceptK8s(
Expand Down Expand Up @@ -42,4 +50,87 @@ describe('Home page Projects section', () => {
cy.findByTestId('landing-page-projects-empty').should('be.visible');
cy.findByTestId('create-project-button').should('not.exist');
});
it('should show create project button when more projects exist', () => {
initHomeIntercepts({ disableHome: false });
const projectsMock = mockProjectsK8sList();

cy.interceptK8sList(ProjectModel, projectsMock);

cy.visit('/');

cy.findByTestId('create-project').should('be.visible');
cy.findByTestId('create-project-card').should('not.exist');
});
it('should not show create project button when more projects exist but user is not allowed', () => {
initHomeIntercepts({ disableHome: false });
interceptAccessReview(false);
const projectsMock = mockProjectsK8sList();

cy.interceptK8sList(ProjectModel, projectsMock);

cy.visit('/');

cy.findByTestId('create-project').should('not.exist');
cy.findByTestId('create-project-card').should('not.exist');
cy.findByTestId('request-project-help').should('be.visible');
cy.findByTestId('request-project-card').should('not.exist');
});
it('should show create project card when no more projects exist', () => {
initHomeIntercepts({ disableHome: false });
const projectsMock = mockProjectsK8sList();
const projects = projectsMock.items;
projectsMock.items = projects.slice(0, 2);

cy.interceptK8sList(ProjectModel, projectsMock);

cy.visit('/');

cy.findByTestId('create-project').should('not.exist');
cy.findByTestId('create-project-card').should('be.visible');
});
it('should show a request project card when no more projects exist but user is not allowed', () => {
initHomeIntercepts({ disableHome: false });
interceptAccessReview(false);
const projectsMock = mockProjectsK8sList();
const projects = projectsMock.items;
projectsMock.items = projects.slice(0, 2);

cy.interceptK8sList(ProjectModel, projectsMock);

cy.visit('/');

cy.findByTestId('create-project').should('not.exist');
cy.findByTestId('create-project-card').should('not.exist');
cy.findByTestId('request-project-card').should('be.visible');
cy.findByTestId('request-project-help').should('not.exist');
});
it('should navigate to the project when the name is clicked', () => {
initHomeIntercepts({ disableHome: false });
interceptAccessReview(false);
const projectsMock = mockProjectsK8sList();
const projects = projectsMock.items;
projectsMock.items = projects.slice(0, 2);

cy.interceptK8sList(ProjectModel, projectsMock);

cy.visit('/');

cy.findByTestId(`project-link-${projects[0].metadata.name}`).click();
cy.url().should('include', projects[0].metadata.name);
projectDetails.findComponent('overview').should('be.visible');
});
it('should navigate to the project list', () => {
initHomeIntercepts({ disableHome: false });
interceptAccessReview(false);
const projectsMock = mockProjectsK8sList();
const projects = projectsMock.items;
projectsMock.items = projects.slice(0, 2);

cy.interceptK8sList(ProjectModel, projectsMock);

cy.visit('/');

cy.findByTestId('goto-projects-link').click();
projectListPage.findProjectsTable().should('be.visible');
});
});
32 changes: 32 additions & 0 deletions frontend/src/components/EvenlySpacedGallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';
import { Gallery, GalleryProps } from '@patternfly/react-core';

type EvenlySpacedGalleryProps = Omit<GalleryProps, 'minWidths' | 'maxWidths'> & {
minSize?: string;
itemCount: number;
};

const EvenlySpacedGallery: React.FC<EvenlySpacedGalleryProps> = ({
minSize,
itemCount,
hasGutter,
children,
...rest
}) => {
const galleryWidths = `calc(${100 / itemCount}%${
hasGutter ? ` - (1rem * ${itemCount - 1} / ${itemCount})` : ''
})`;

return (
<Gallery
hasGutter={hasGutter}
minWidths={{ default: minSize || galleryWidths }}
maxWidths={{ default: galleryWidths }}
{...rest}
>
{children}
</Gallery>
);
};

export default EvenlySpacedGallery;
9 changes: 0 additions & 9 deletions frontend/src/components/OdhCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,6 @@
align-items: center;
}

.pf-v5-c-card__body {
.odh-card__body-text {
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
}

&__coming-soon {
color: var(--pf-v5-global--disabled-color--100);
font-size: var(--pf-v5-global--FontSize--md);
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/OdhDocCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import BrandImage from './BrandImage';
import DocCardBadges from './DocCardBadges';
import { useQuickStartCardSelected } from './useQuickStartCardSelected';
import FavoriteButton from './FavoriteButton';
import TruncatedText from './TruncatedText';

import './OdhCard.scss';

Expand Down Expand Up @@ -179,7 +180,7 @@ const OdhDocCard: React.FC<OdhDocCardProps> = ({ odhDoc, favorite, updateFavorit
</StackItem>
<StackItem>
<Tooltip content={odhDoc.spec.description}>
<span className="odh-card__body-text">{odhDoc.spec.description}</span>
<TruncatedText maxLines={4} content={odhDoc.spec.description} />
</Tooltip>
</StackItem>
</Stack>
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/TruncatedText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { Tooltip } from '@patternfly/react-core';

type TruncatedTextProps = {
maxLines: number;
content: string;
} & React.HTMLProps<HTMLSpanElement>;

const TruncatedText: React.FC<TruncatedTextProps> = ({ maxLines, content, ...rest }) => (
<Tooltip content={content}>
<span
style={{
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: maxLines,
}}
{...rest}
>
{content}
</span>
</Tooltip>
);

export default TruncatedText;
14 changes: 10 additions & 4 deletions frontend/src/concepts/projects/ProjectsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { KnownLabels, ProjectKind } from '~/k8sTypes';
import { useDashboardNamespace } from '~/redux/selectors';
import { isAvailableProject } from '~/concepts/projects/utils';

const getProjectDisplayName = (resource: ProjectKind): string =>
resource.metadata.annotations?.['openshift.io/display-name'] || resource.metadata.name;

const projectSorter = (projectA: ProjectKind, projectB: ProjectKind) =>
getProjectDisplayName(projectA).localeCompare(getProjectDisplayName(projectB));

type ProjectFetchState = FetchState<ProjectKind[]>;
type ProjectsContextType = {
projects: ProjectKind[];
Expand Down Expand Up @@ -128,10 +134,10 @@ const ProjectsContextProvider: React.FC<ProjectsProviderProps> = ({ children })

const contextValue = React.useMemo(
() => ({
projects,
dataScienceProjects,
modelServingProjects,
nonActiveProjects,
projects: projects.sort(projectSorter),
dataScienceProjects: dataScienceProjects.sort(projectSorter),
modelServingProjects: modelServingProjects.sort(projectSorter),
nonActiveProjects: nonActiveProjects.sort(projectSorter),
preferredProject,
updatePreferredProject,
loaded,
Expand Down
15 changes: 4 additions & 11 deletions frontend/src/pages/home/aiFlows/useAIFlows.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as React from 'react';
import { Gallery, PageSection, Stack, Text, TextContent } from '@patternfly/react-core';
import { 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 EvenlySpacedGallery from '~/components/EvenlySpacedGallery';
import ProjectsGallery from './ProjectsGallery';
import CreateAndTrainGallery from './CreateAndTrainGallery';
import DeployAndMonitorGallery from './DeployAndMonitorGallery';
Expand Down Expand Up @@ -67,23 +68,15 @@ export const useAIFlows = (): React.ReactNode => {
return null;
}

const galleryWidths = `calc(${100 / cards.length}% - (1rem * ${cards.length - 1} / ${
cards.length
}))`;

return (
<PageSection data-testid="home-page-ai-flows" variant="light">
<Stack hasGutter>
<TextContent>
<Text component="h1">Train, serve, monitor, and manage AI/ML models</Text>
</TextContent>
<Gallery
hasGutter
minWidths={{ default: '100%', md: galleryWidths }}
maxWidths={{ default: '100%', md: galleryWidths }}
>
<EvenlySpacedGallery itemCount={cards.length} hasGutter>
{cards}
</Gallery>
</EvenlySpacedGallery>
{selected === 'organize' ? (
<ProjectsGallery onClose={() => setSelected(undefined)} />
) : null}
Expand Down
63 changes: 63 additions & 0 deletions frontend/src/pages/home/projects/CreateProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from 'react';
import {
Bullseye,
Button,
CardBody,
Flex,
FlexItem,
Text,
TextContent,
} from '@patternfly/react-core';
import { ProjectObjectType, SectionType, typedObjectImage } from '~/concepts/design/utils';
import TypeBorderedCard from '~/concepts/design/TypeBorderedCard';

interface CreateProjectCardProps {
allowCreate: boolean;
onCreateProject: () => void;
}

const CreateProjectCard: React.FC<CreateProjectCardProps> = ({ allowCreate, onCreateProject }) => (
<TypeBorderedCard
data-testid={allowCreate ? 'create-project-card' : 'request-project-card'}
sectionType={SectionType.organize}
>
<CardBody>
<Bullseye>
<Flex direction={{ default: 'column' }} alignItems={{ default: 'alignItemsCenter' }}>
<FlexItem>
<img
src={typedObjectImage(ProjectObjectType.project)}
alt="Add project"
width={54}
height={54}
/>
</FlexItem>
{allowCreate ? (
<FlexItem>
<Button variant="link" isInline onClick={onCreateProject}>
Create project
</Button>
</FlexItem>
) : (
<>
<FlexItem>
<TextContent>
<Text component="h3">Need another project?</Text>
</TextContent>
</FlexItem>
<FlexItem>
<TextContent>
<Text component="small" style={{ textAlign: 'center' }}>
Contact your administrator to request a project creation for you.
</Text>
</TextContent>
</FlexItem>
</>
)}
</Flex>
</Bullseye>
</CardBody>
</TypeBorderedCard>
);

export default CreateProjectCard;
Loading

0 comments on commit 2ae6e4e

Please sign in to comment.