Skip to content

Commit

Permalink
Update projects empty state page and Launch Jupyter button (#3173)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 authored Sep 11, 2024
1 parent 39b023e commit 2e37911
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 50 deletions.
2 changes: 1 addition & 1 deletion frontend/src/__tests__/cypress/cypress/pages/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class ProjectListPage {

class CreateEditProjectModal extends Modal {
constructor(private edit = false) {
super(`${edit ? 'Edit' : 'Create'} data science project`);
super(`${edit ? 'Edit' : 'Create'} project`);
}

findNameInput() {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/concepts/design/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import deployingModelsImg from '~/images/UI_icon-Red_Hat-Server_upload-RGB.svg';
import dataConnectionImg from '~/images/UI_icon-Red_Hat-Connected-RGB.svg';
import userImg from '~/images/UI_icon-Red_Hat-User-RGB.svg';
import groupImg from '~/images/UI_icon-Red_Hat-Shared_workspace-RGB.svg';
import projectEmptyStateImg from '~/images/empty-state-projects-overview.svg';
import projectEmptyStateImg from '~/images/empty-state-project-overview.svg';
import notebookEmptyStateImg from '~/images/empty-state-notebooks.svg';
import pipelineEmptyStateImg from '~/images/empty-state-pipelines.svg';
import clusterStorageEmptyStateImg from '~/images/empty-state-cluster-storage.svg';
Expand Down
275 changes: 275 additions & 0 deletions frontend/src/images/empty-state-projects-color.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 52 additions & 22 deletions frontend/src/pages/projects/screens/projects/EmptyProjects.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import * as React from 'react';
import {
ButtonVariant,
EmptyState,
EmptyStateBody,
EmptyStateIcon,
EmptyStateActions,
EmptyStateHeader,
EmptyStateFooter,
Popover,
Button,
Icon,
TextContent,
TextList,
TextListItem,
} from '@patternfly/react-core';
import { CubesIcon } from '@patternfly/react-icons';
import { useNavigate } from 'react-router-dom';
import { ODH_PRODUCT_NAME } from '~/utilities/const';
import LaunchJupyterButton from '~/pages/projects/screens/projects/LaunchJupyterButton';
import { useCheckJupyterEnabled } from '~/utilities/notebookControllerUtils';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import projectsEmptyStateImg from '~/images/empty-state-projects-color.svg';
import NewProjectButton from './NewProjectButton';

type EmptyProjectsProps = {
Expand All @@ -21,36 +23,64 @@ type EmptyProjectsProps = {

const EmptyProjects: React.FC<EmptyProjectsProps> = ({ allowCreate }) => {
const navigate = useNavigate();
const isJupyterEnabled = useCheckJupyterEnabled();

return (
<EmptyState>
<EmptyState variant="lg">
<EmptyStateHeader
data-testid="no-data-science-project"
titleText="No data science projects yet."
icon={<EmptyStateIcon icon={CubesIcon} />}
titleText={allowCreate ? 'Start by creating your project' : 'Start by requesting a project'}
icon={
<EmptyStateIcon
icon={() => (
<img style={{ height: 320 }} src={projectsEmptyStateImg} alt="no projects" />
)}
/>
}
headingLevel="h2"
/>
<EmptyStateBody>
{allowCreate
? `To get started, create a data science project${
isJupyterEnabled ? ' or launch a notebook with Jupyter' : ''
}.`
: `To get started, ask your ${ODH_PRODUCT_NAME} admin for a data science project${
isJupyterEnabled ? ' or launch a notebook with Jupyter' : ''
}.`}
<EmptyStateBody data-testid="projects-empty-body-text">
Projects allow you and your team to organize and collaborate on resources within separate
namespaces.{!allowCreate ? ' To request a project, contact your administrator.' : ''}
</EmptyStateBody>
<EmptyStateFooter>
{allowCreate ? (
<>
<NewProjectButton
onProjectCreated={(projectName) => navigate(`/projects/${projectName}`)}
/>
<EmptyStateActions>
<LaunchJupyterButton variant={ButtonVariant.link} />
</EmptyStateActions>
</>
) : (
<LaunchJupyterButton variant={ButtonVariant.primary} />
<Popover
minWidth="400px"
headerContent="Your administrator might be:"
bodyContent={
<TextContent data-testid="projects-empty-admin-help-content">
<TextList>
<TextListItem>The person who gave you your username</TextListItem>
<TextListItem>
Someone in your IT department or Help desk (at a company or school)
</TextListItem>
<TextListItem>
The person who manages your email service or web site (in a small business or
club)
</TextListItem>
</TextList>
</TextContent>
}
>
<Button
data-testid="projects-empty-admin-help"
isInline
variant="link"
icon={
<Icon isInline aria-label="More info">
<OutlinedQuestionCircleIcon />
</Icon>
}
>
{`Who's my administrator?`}
</Button>
</Popover>
)}
</EmptyStateFooter>
</EmptyState>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import * as React from 'react';
import { Button, ButtonVariant } from '@patternfly/react-core';
import { Button, ButtonVariant, Tooltip } from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { useCheckJupyterEnabled } from '~/utilities/notebookControllerUtils';

type LaunchJupyterButtonProps = {
variant: ButtonVariant;
};

const LaunchJupyterButton: React.FC<LaunchJupyterButtonProps> = ({ variant }) => {
const LaunchJupyterButton: React.FC = () => {
const navigate = useNavigate();
const isJupyterEnabled = useCheckJupyterEnabled();

Expand All @@ -16,16 +12,22 @@ const LaunchJupyterButton: React.FC<LaunchJupyterButtonProps> = ({ variant }) =>
}

return (
<Button
href="/notebookController"
variant={variant}
onClick={(e) => {
e.preventDefault();
navigate('/notebookController');
}}
<Tooltip
position="left"
content="Launch a notebook server to create a standalone notebook outside of a project."
>
Launch Jupyter
</Button>
<Button
href="/notebookController"
component="a"
variant={ButtonVariant.secondary}
onClick={(e) => {
e.preventDefault();
navigate('/notebookController');
}}
>
Launch standalone notebook server
</Button>
</Tooltip>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const ManageProjectModal: React.FC<ManageProjectModalProps> = ({

return (
<Modal
title={editProjectData ? 'Edit data science project' : 'Create data science project'}
title={editProjectData ? 'Edit project' : 'Create project'}
variant="medium"
isOpen={open}
onClose={() => onBeforeClose()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const NewProjectButton: React.FC<NewProjectButtonProps> = ({ closeOnCreate, onPr
variant="primary"
onClick={() => setOpen(true)}
>
Create data science project
Create project
</Button>
<ManageProjectModal
open={open}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import * as React from 'react';
import { Button, ButtonVariant, ToolbarGroup, ToolbarItem } from '@patternfly/react-core';
import { Button, ToolbarGroup, ToolbarItem } from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { Table } from '~/components/table';
import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField';
import { ProjectKind } from '~/k8sTypes';
import { getProjectOwner } from '~/concepts/projects/utils';
import { useAppContext } from '~/app/AppContext';
import LaunchJupyterButton from '~/pages/projects/screens/projects/LaunchJupyterButton';
import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
import ProjectTableRow from '~/pages/projects/screens/projects/ProjectTableRow';
import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils';
Expand All @@ -20,7 +18,6 @@ type ProjectListViewProps = {
};

const ProjectListView: React.FC<ProjectListViewProps> = ({ allowCreate }) => {
const { dashboardConfig } = useAppContext();
const { projects } = React.useContext(ProjectsContext);
const navigate = useNavigate();
const [searchType, setSearchType] = React.useState<SearchType>(SearchType.NAME);
Expand Down Expand Up @@ -94,11 +91,6 @@ const ProjectListView: React.FC<ProjectListViewProps> = ({ allowCreate }) => {
</ToolbarItem>
</ToolbarGroup>
<ToolbarGroup align={{ default: 'alignRight' }}>
{dashboardConfig.spec.notebookController?.enabled && (
<ToolbarItem>
<LaunchJupyterButton variant={ButtonVariant.link} />
</ToolbarItem>
)}
{allowCreate && (
<ToolbarItem>
<NewProjectButton
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/pages/projects/screens/projects/ProjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { AccessReviewResourceAttributes } from '~/k8sTypes';
import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
import { ProjectObjectType } from '~/concepts/design/utils';
import TitleWithIcon from '~/concepts/design/TitleWithIcon';
import LaunchJupyterButton from '~/pages/projects/screens/projects/LaunchJupyterButton';
import { useAppContext } from '~/app/AppContext';
import EmptyProjects from './EmptyProjects';
import ProjectListView from './ProjectListView';

Expand All @@ -15,12 +17,16 @@ const accessReviewResource: AccessReviewResourceAttributes = {
};

const ProjectView: React.FC = () => {
const { dashboardConfig } = useAppContext();
const { projects } = React.useContext(ProjectsContext);
const [allowCreate, rbacLoaded] = useAccessReview(accessReviewResource);

return (
<ApplicationsPage
title={<TitleWithIcon title="Data Science Projects" objectType={ProjectObjectType.project} />}
headerAction={
dashboardConfig.spec.notebookController?.enabled ? <LaunchJupyterButton /> : undefined
}
description={
rbacLoaded
? `View your existing projects${allowCreate ? ' or create new projects' : ''}.`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { act, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import EmptyProjects from '~/pages/projects/screens/projects/EmptyProjects';
import { useUser } from '~/redux/selectors';

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('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(),
}));

const useUserMock = jest.mocked(useUser);
useUserMock.mockReturnValue({
username: 'test-user',
isAdmin: false,
isAllowed: true,
userLoading: false,
userError: null,
});

describe('EmptyProjects', () => {
it('should show the create project button when allowed to create projects', async () => {
render(<EmptyProjects allowCreate />);
const createProject = await screen.findByTestId('create-data-science-project');
expect(createProject).toBeEnabled();
const bodyText = await screen.findByTestId('projects-empty-body-text');
expect(bodyText).toHaveTextContent(
'Projects allow you and your team to organize and collaborate on resources within separate namespaces.',
);
expect(bodyText).not.toHaveTextContent('To request a project, contact your administrator.');
});
it('should show the who is my admin help when not allowed to create projects', async () => {
render(<EmptyProjects allowCreate={false} />);
const bodyText = await screen.findByTestId('projects-empty-body-text');
expect(bodyText).toHaveTextContent(
'Projects allow you and your team to organize and collaborate on resources within separate namespaces. To request a project, contact your administrator.',
);
const adminHelpButton = await screen.findByTestId('projects-empty-admin-help');
act(() => adminHelpButton.click());
const helpContent = await screen.findByTestId('projects-empty-admin-help-content');
expect(helpContent).toBeVisible();
});
});

0 comments on commit 2e37911

Please sign in to comment.