Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RHOAIENG-5489] Home page: projects w/ tiles #2741

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting -- FWIW, if you provide style in rest you'll lose the primary purpose of the component

>
{content}
</span>
</Tooltip>
);

export default TruncatedText;
13 changes: 8 additions & 5 deletions frontend/src/concepts/projects/ProjectsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { useProjects } from '~/api';
import { FetchState } from '~/utilities/useFetchState';
import { KnownLabels, ProjectKind } from '~/k8sTypes';
import { useDashboardNamespace } from '~/redux/selectors';
import { isAvailableProject } from '~/concepts/projects/utils';
import { getProjectDisplayName, isAvailableProject } from '~/concepts/projects/utils';

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

type ProjectFetchState = FetchState<ProjectKind[]>;
type ProjectsContextType = {
Expand Down Expand Up @@ -128,10 +131,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>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This font size looks really large relative to other contents on the page.
I'm also not seeing an h2 level heading after the h1 for the "Projects" section header.

image

One suggestion here is to use the h2 combined with a utility class for the font size:

<Text component="h3" class="pf-v5-u-font-size-sm" >Need another project?</Text>

When I look at the current design document, the sm size is used for this 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
Loading