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

Add project view details page #578

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
4 changes: 2 additions & 2 deletions frontend/src/app/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ExploreApplications = React.lazy(
const NotebookLogoutRedirectPage = React.lazy(
() => import('../pages/notebookController/NotebookLogoutRedirect'),
);
const ProjectView = React.lazy(() => import('../pages/projects/ProjectView'));
const ProjectViewRoutes = React.lazy(() => import('../pages/projects/ProjectViewRoutes'));
const NotebookController = React.lazy(
() => import('../pages/notebookController/NotebookController'),
);
Expand All @@ -36,7 +36,7 @@ const Routes: React.FC = () => {
<Route path="/resources" exact component={LearningCenterPage} />
{isAllowed && (
<Route path="/projects">
<ProjectView />
<ProjectViewRoutes />
</Route>
)}
{isAllowed && (
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/pages/projects/ProjectViewRoutes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom';
import ProjectDetails from './screens/detail/ProjectDetails';
import ProjectView from './ProjectView';

const ProjectViewRoutes: React.FC = () => {
const { path } = useRouteMatch();

return (
<Switch>
<Route exact path={path}>
<ProjectView />
</Route>
<Route exact path={`${path}/:namespace`}>
<ProjectDetails />
</Route>
<Route>
<Redirect to={path} />
</Route>
</Switch>
);
};

export default ProjectViewRoutes;
27 changes: 27 additions & 0 deletions frontend/src/pages/projects/screens/detail/DataConnectionsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import { Button } from '@patternfly/react-core';
import EmptyDetailsList from './EmptyDetailsList';
import { ProjectSectionID } from './types';
import DetailsSection from './DetailsSection';
import { ProjectSectionTitles } from './const';

const DataConnectionsList: React.FC = () => {
return (
<DetailsSection
id={ProjectSectionID.DATA_CONNECTIONS}
title={ProjectSectionTitles[ProjectSectionID.DATA_CONNECTIONS]}
actions={[
<Button key={`action-${ProjectSectionID.DATA_CONNECTIONS}`} variant="secondary">
Add data connection
</Button>,
]}
>
<EmptyDetailsList
title="No data connections"
description="To get started, add data to your project."
/>
</DetailsSection>
);
};

export default DataConnectionsList;
29 changes: 29 additions & 0 deletions frontend/src/pages/projects/screens/detail/DetailsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import { Flex, FlexItem, Stack, StackItem, Title } from '@patternfly/react-core';
import { ProjectSectionID } from './types';

type DetailsSectionProps = {
id: ProjectSectionID;
actions: React.ReactNode[];
title: string;
};

const DetailsSection: React.FC<DetailsSectionProps> = ({ children, id, title, actions }) => {
return (
<Stack hasGutter>
<StackItem>
<Flex>
<FlexItem>
<Title id={id} headingLevel="h4" size="xl">
{title}
</Title>
</FlexItem>
<FlexItem>{actions}</FlexItem>
</Flex>
</StackItem>
<StackItem>{children}</StackItem>
</Stack>
);
};

export default DetailsSection;
22 changes: 22 additions & 0 deletions frontend/src/pages/projects/screens/detail/EmptyDetailsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react';
import { EmptyState, EmptyStateBody, EmptyStateIcon, Title } from '@patternfly/react-core';
import { CubesIcon } from '@patternfly/react-icons';

type EmptyDetailsListProps = {
title: string;
description: string;
};

const EmptyDetailsList: React.FC<EmptyDetailsListProps> = ({ title, description }) => {
return (
<EmptyState variant="xs">
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel="h5" size="lg">
{title}
</Title>
<EmptyStateBody>{description}</EmptyStateBody>
</EmptyState>
);
};

export default EmptyDetailsList;
59 changes: 59 additions & 0 deletions frontend/src/pages/projects/screens/detail/ProjectDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Divider, PageSection, Stack, StackItem } from '@patternfly/react-core';
import * as React from 'react';
import { useParams } from 'react-router';
import ApplicationsPage from '../../../ApplicationsPage';
import EmptyProjects from '../../EmptyProjects';
import DataConnectionsList from './DataConnectionsList';
import ProjectDetailsSidebar from './ProjectDetailsSidebar';
import StorageList from './StorageList';
import { ProjectSectionID } from './types';
import WorkspacesList from './WorkspacesList';

type SectionType = {
id: ProjectSectionID;
component: React.ReactNode;
};

const ProjectDetails: React.FC = () => {
const { namespace } = useParams<{ namespace: string }>();

const sections: SectionType[] = [
{ id: ProjectSectionID.WORKSPACE, component: <WorkspacesList /> },
{ id: ProjectSectionID.STORAGE, component: <StorageList /> },
{ id: ProjectSectionID.DATA_CONNECTIONS, component: <DataConnectionsList /> },
];

const mapSections = (
id: ProjectSectionID,
component: React.ReactNode,
index: number,
array: SectionType[],
Comment on lines +29 to +30
Copy link
Member

Choose a reason for hiding this comment

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

If you're making another function -- why not just compute this as "last item" or something?

) => (
<React.Fragment key={id}>
<StackItem>{component}</StackItem>
{index !== array.length - 1 && <Divider />}
</React.Fragment>
);

return (
<ApplicationsPage
title={`${namespace} project details`}
description={null}
loaded
empty={false}
emptyStatePage={<EmptyProjects />}
>
<PageSection id="project-details-list" hasOverflowScroll variant="light">
<ProjectDetailsSidebar>
<Stack hasGutter>
{sections.map(({ id, component }, index, array) =>
mapSections(id, component, index, array),
)}
</Stack>
</ProjectDetailsSidebar>
</PageSection>
</ApplicationsPage>
);
};

export default ProjectDetails;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import {
JumpLinks,
JumpLinksItem,
Sidebar,
SidebarContent,
SidebarPanel,
} from '@patternfly/react-core';
import { ProjectSectionID } from './types';
import { ProjectSectionTitles } from './const';

const ProjectDetailsSidebar: React.FC = ({ children }) => {
return (
<Sidebar hasGutter>
<SidebarPanel variant="sticky">
<JumpLinks isVertical label="Jump to section" scrollableSelector="#project-details-list">
{Object.values(ProjectSectionID).map((section) => (
<JumpLinksItem key={section} href={`#${section}`}>
{ProjectSectionTitles[section]}
</JumpLinksItem>
))}
</JumpLinks>
</SidebarPanel>
<SidebarContent>{children}</SidebarContent>
</Sidebar>
);
};

export default ProjectDetailsSidebar;
27 changes: 27 additions & 0 deletions frontend/src/pages/projects/screens/detail/StorageList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import { Button } from '@patternfly/react-core';
import EmptyDetailsList from './EmptyDetailsList';
import DetailsSection from './DetailsSection';
import { ProjectSectionID } from './types';
import { ProjectSectionTitles } from './const';

const StorageList: React.FC = () => {
return (
<DetailsSection
id={ProjectSectionID.STORAGE}
title={ProjectSectionTitles[ProjectSectionID.STORAGE]}
actions={[
<Button key={`action-${ProjectSectionID.STORAGE}`} variant="secondary">
Add storage
</Button>,
]}
>
<EmptyDetailsList
title="No storage"
description="Choose existing, or add new on cluster storage."
/>
</DetailsSection>
);
};

export default StorageList;
27 changes: 27 additions & 0 deletions frontend/src/pages/projects/screens/detail/WorkspacesList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import { Button } from '@patternfly/react-core';
import EmptyDetailsList from './EmptyDetailsList';
import { ProjectSectionID } from './types';
import DetailsSection from './DetailsSection';
import { ProjectSectionTitles } from './const';

const WorkspacesList: React.FC = () => {
return (
<DetailsSection
id={ProjectSectionID.WORKSPACE}
title={ProjectSectionTitles[ProjectSectionID.WORKSPACE]}
actions={[
<Button key={`action-${ProjectSectionID.WORKSPACE}`} variant="secondary">
Create data science workspace
</Button>,
]}
>
<EmptyDetailsList
title="No data science workspaces"
description="To get started, create a data science workspace."
/>
</DetailsSection>
);
};

export default WorkspacesList;
7 changes: 7 additions & 0 deletions frontend/src/pages/projects/screens/detail/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ProjectSectionID, ProjectSectionTitlesType } from './types';

export const ProjectSectionTitles: ProjectSectionTitlesType = {
[ProjectSectionID.WORKSPACE]: 'Data science workspaces',
[ProjectSectionID.STORAGE]: 'Storage',
[ProjectSectionID.DATA_CONNECTIONS]: 'Data connections',
};
9 changes: 9 additions & 0 deletions frontend/src/pages/projects/screens/detail/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum ProjectSectionID {
WORKSPACE = 'data-science-workspaces',
STORAGE = 'storage',
DATA_CONNECTIONS = 'data-connections',
}

export type ProjectSectionTitlesType = {
[key in ProjectSectionID]: string;
};