From 8b1334aa40c08755f254ed02c935945c5715c0a8 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Mon, 19 Aug 2024 15:23:41 -0400 Subject: [PATCH] Update projects empty state page and Launch Jupyter button --- frontend/src/concepts/design/utils.ts | 2 +- ...w.svg => empty-state-project-overview.svg} | 0 .../src/images/empty-state-projects-color.svg | 275 ++++++++++++++++++ .../screens/projects/EmptyProjects.tsx | 68 +++-- .../screens/projects/LaunchJupyterButton.tsx | 31 +- .../screens/projects/ManageProjectModal.tsx | 2 +- .../screens/projects/NewProjectButton.tsx | 2 +- .../screens/projects/ProjectListView.tsx | 10 +- .../projects/screens/projects/ProjectView.tsx | 6 + .../projects/__tests__/EmptyProjects.spec.tsx | 53 ++++ 10 files changed, 400 insertions(+), 49 deletions(-) rename frontend/src/images/{empty-state-projects-overview.svg => empty-state-project-overview.svg} (100%) create mode 100644 frontend/src/images/empty-state-projects-color.svg create mode 100644 frontend/src/pages/projects/screens/projects/__tests__/EmptyProjects.spec.tsx diff --git a/frontend/src/concepts/design/utils.ts b/frontend/src/concepts/design/utils.ts index eb53428ee6..8e4e91da87 100644 --- a/frontend/src/concepts/design/utils.ts +++ b/frontend/src/concepts/design/utils.ts @@ -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'; diff --git a/frontend/src/images/empty-state-projects-overview.svg b/frontend/src/images/empty-state-project-overview.svg similarity index 100% rename from frontend/src/images/empty-state-projects-overview.svg rename to frontend/src/images/empty-state-project-overview.svg diff --git a/frontend/src/images/empty-state-projects-color.svg b/frontend/src/images/empty-state-projects-color.svg new file mode 100644 index 0000000000..04f4244695 --- /dev/null +++ b/frontend/src/images/empty-state-projects-color.svg @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/pages/projects/screens/projects/EmptyProjects.tsx b/frontend/src/pages/projects/screens/projects/EmptyProjects.tsx index 37a2dba510..58b8f3ee01 100644 --- a/frontend/src/pages/projects/screens/projects/EmptyProjects.tsx +++ b/frontend/src/pages/projects/screens/projects/EmptyProjects.tsx @@ -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 = { @@ -21,23 +23,24 @@ type EmptyProjectsProps = { const EmptyProjects: React.FC = ({ allowCreate }) => { const navigate = useNavigate(); - const isJupyterEnabled = useCheckJupyterEnabled(); + return ( - + } + titleText={allowCreate ? 'Start by creating your project' : 'Start by requesting a project'} + icon={ + ( + no projects + )} + /> + } headingLevel="h2" /> - - {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' : '' - }.`} + + Projects allow you and your team to organize and collaborate on resources within separate + namespaces.{!allowCreate ? ' To request a project, contact your administrator.' : ''} {allowCreate ? ( @@ -45,12 +48,33 @@ const EmptyProjects: React.FC = ({ allowCreate }) => { navigate(`/projects/${projectName}`)} /> - - - ) : ( - + + + The person who gave you your username + + Someone in your IT department or Help desk (at a company or school) + + + The person who manages your email service or web site (in a small business or + club) + + + + } + > + + )} diff --git a/frontend/src/pages/projects/screens/projects/LaunchJupyterButton.tsx b/frontend/src/pages/projects/screens/projects/LaunchJupyterButton.tsx index 4800582867..94fe93ed79 100644 --- a/frontend/src/pages/projects/screens/projects/LaunchJupyterButton.tsx +++ b/frontend/src/pages/projects/screens/projects/LaunchJupyterButton.tsx @@ -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 = ({ variant }) => { +const LaunchJupyterButton: React.FC = () => { const navigate = useNavigate(); const isJupyterEnabled = useCheckJupyterEnabled(); @@ -16,16 +12,21 @@ const LaunchJupyterButton: React.FC = ({ variant }) => } return ( - + + ); }; diff --git a/frontend/src/pages/projects/screens/projects/ManageProjectModal.tsx b/frontend/src/pages/projects/screens/projects/ManageProjectModal.tsx index c7cb62fe36..e994567797 100644 --- a/frontend/src/pages/projects/screens/projects/ManageProjectModal.tsx +++ b/frontend/src/pages/projects/screens/projects/ManageProjectModal.tsx @@ -92,7 +92,7 @@ const ManageProjectModal: React.FC = ({ return ( onBeforeClose()} diff --git a/frontend/src/pages/projects/screens/projects/NewProjectButton.tsx b/frontend/src/pages/projects/screens/projects/NewProjectButton.tsx index 72f5d5a79e..1d3697dd40 100644 --- a/frontend/src/pages/projects/screens/projects/NewProjectButton.tsx +++ b/frontend/src/pages/projects/screens/projects/NewProjectButton.tsx @@ -17,7 +17,7 @@ const NewProjectButton: React.FC = ({ closeOnCreate, onPr variant="primary" onClick={() => setOpen(true)} > - Create data science project + Create project = ({ allowCreate }) => { - const { dashboardConfig } = useAppContext(); const { projects } = React.useContext(ProjectsContext); const navigate = useNavigate(); const [searchType, setSearchType] = React.useState(SearchType.NAME); @@ -94,11 +91,6 @@ const ProjectListView: React.FC = ({ allowCreate }) => { - {dashboardConfig.spec.notebookController?.enabled && ( - - - - )} {allowCreate && ( { + const { dashboardConfig } = useAppContext(); const { projects } = React.useContext(ProjectsContext); const [allowCreate, rbacLoaded] = useAccessReview(accessReviewResource); return ( } + headerAction={ + dashboardConfig.spec.notebookController?.enabled ? : undefined + } description={ rbacLoaded ? `View your existing projects${allowCreate ? ' or create new projects' : ''}.` diff --git a/frontend/src/pages/projects/screens/projects/__tests__/EmptyProjects.spec.tsx b/frontend/src/pages/projects/screens/projects/__tests__/EmptyProjects.spec.tsx new file mode 100644 index 0000000000..4397c6b49c --- /dev/null +++ b/frontend/src/pages/projects/screens/projects/__tests__/EmptyProjects.spec.tsx @@ -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(); + 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(); + 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(); + }); +});