feat(ws): Notebooks 2.0 // Frontend // Workspaces details // Live mockup (#174)
Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com>
This commit is contained in:
parent
249a45677d
commit
d84621aac1
|
@ -43,6 +43,7 @@ const App: React.FC = () => {
|
|||
<Page
|
||||
mainContainerId="primary-app-container"
|
||||
masthead={masthead}
|
||||
isContentFilled
|
||||
isManagedSidebar
|
||||
sidebar={<NavSidebar />}
|
||||
>
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
DrawerActions,
|
||||
DrawerCloseButton,
|
||||
DrawerHead,
|
||||
DrawerPanelBody,
|
||||
DrawerPanelContent,
|
||||
Tabs,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import { Workspace } from '~/shared/types';
|
||||
import { WorkspaceDetailsOverview } from '~/app/pages/Workspaces/Details/WorkspaceDetailsOverview';
|
||||
import { WorkspaceDetailsActions } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActions';
|
||||
|
||||
type WorkspaceDetailsProps = {
|
||||
workspace: Workspace;
|
||||
onCloseClick: React.MouseEventHandler;
|
||||
onEditClick: React.MouseEventHandler;
|
||||
onDeleteClick: React.MouseEventHandler;
|
||||
};
|
||||
|
||||
export const WorkspaceDetails: React.FunctionComponent<WorkspaceDetailsProps> = ({
|
||||
workspace,
|
||||
onCloseClick,
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
}) => {
|
||||
const [activeTabKey, setActiveTabKey] = React.useState<string | number>(0);
|
||||
|
||||
const handleTabClick = (
|
||||
event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
|
||||
tabIndex: string | number,
|
||||
) => {
|
||||
setActiveTabKey(tabIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerPanelContent isResizable defaultSize="50%">
|
||||
<DrawerHead>
|
||||
<Title headingLevel="h6">{workspace.name}</Title>
|
||||
<WorkspaceDetailsActions onEditClick={onEditClick} onDeleteClick={onDeleteClick} />
|
||||
<DrawerActions>
|
||||
<DrawerCloseButton onClick={onCloseClick} />
|
||||
</DrawerActions>
|
||||
</DrawerHead>
|
||||
<DrawerPanelBody>
|
||||
<Tabs activeKey={activeTabKey} onSelect={handleTabClick}>
|
||||
<Tab eventKey={0} title={<TabTitleText>Overview</TabTitleText>} aria-label="Overview">
|
||||
<WorkspaceDetailsOverview workspace={workspace} />
|
||||
</Tab>
|
||||
<Tab eventKey={1} title={<TabTitleText>Activity</TabTitleText>} aria-label="Activity">
|
||||
Activity
|
||||
</Tab>
|
||||
<Tab eventKey={2} title={<TabTitleText>Logs</TabTitleText>} aria-label="Logs">
|
||||
Logs
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={3}
|
||||
title={<TabTitleText>Pod template</TabTitleText>}
|
||||
aria-label="Pod template"
|
||||
>
|
||||
Pod template
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</DrawerPanelBody>
|
||||
</DrawerPanelContent>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownList,
|
||||
MenuToggle,
|
||||
DropdownItem,
|
||||
Flex,
|
||||
FlexItem,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
interface WorkspaceDetailsActionsProps {
|
||||
onEditClick: React.MouseEventHandler;
|
||||
onDeleteClick: React.MouseEventHandler;
|
||||
}
|
||||
|
||||
export const WorkspaceDetailsActions: React.FC<WorkspaceDetailsActionsProps> = ({
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
}) => {
|
||||
const [isOpen, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<FlexItem>
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onSelect={() => setOpen(false)}
|
||||
onOpenChange={(open) => setOpen(open)}
|
||||
popperProps={{ position: 'end' }}
|
||||
toggle={(toggleRef) => (
|
||||
<MenuToggle
|
||||
variant="primary"
|
||||
ref={toggleRef}
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
isExpanded={isOpen}
|
||||
aria-label="Workspace details action toggle"
|
||||
data-testid="workspace-details-action-toggle"
|
||||
>
|
||||
Actions
|
||||
</MenuToggle>
|
||||
)}
|
||||
>
|
||||
<DropdownList>
|
||||
<DropdownItem
|
||||
id="workspace-details-action-edit-button"
|
||||
aria-label="Edit workspace"
|
||||
key="edit-workspace-button"
|
||||
onClick={onEditClick}
|
||||
>
|
||||
Edit
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
id="workspace-details-action-delete-button"
|
||||
aria-label="Delete workspace"
|
||||
key="delete-workspace-button"
|
||||
onClick={onDeleteClick}
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</DropdownList>
|
||||
</Dropdown>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
DescriptionList,
|
||||
DescriptionListTerm,
|
||||
DescriptionListGroup,
|
||||
DescriptionListDescription,
|
||||
} from '@patternfly/react-core';
|
||||
import { Workspace } from '~/shared/types';
|
||||
|
||||
type WorkspaceDetailsOverviewProps = {
|
||||
workspace: Workspace;
|
||||
};
|
||||
|
||||
export const WorkspaceDetailsOverview: React.FunctionComponent<WorkspaceDetailsOverviewProps> = ({
|
||||
workspace,
|
||||
}) => (
|
||||
<DescriptionList>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Name</DescriptionListTerm>
|
||||
<DescriptionListDescription>{workspace.name}</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Kind</DescriptionListTerm>
|
||||
<DescriptionListDescription>{workspace.kind}</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Labels</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{workspace.podTemplate.podMetadata.labels.join(', ')}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Pod config</DescriptionListTerm>
|
||||
<DescriptionListDescription>{workspace.options.podConfig}</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
);
|
|
@ -1,5 +1,8 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerContentBody,
|
||||
PageSection,
|
||||
MenuToggle,
|
||||
TimestampTooltipVariant,
|
||||
|
@ -36,7 +39,7 @@ import {
|
|||
} from '@patternfly/react-table';
|
||||
import { FilterIcon } from '@patternfly/react-icons';
|
||||
import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types';
|
||||
|
||||
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
|
||||
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
|
||||
import { formatRam } from 'shared/utilities/WorkspaceResources';
|
||||
|
||||
|
@ -52,6 +55,10 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
cpu: 3,
|
||||
ram: 500,
|
||||
podTemplate: {
|
||||
podMetadata: {
|
||||
labels: ['label1', 'label2'],
|
||||
annotations: ['annotation1', 'annotation2'],
|
||||
},
|
||||
volumes: {
|
||||
home: '/home',
|
||||
data: [
|
||||
|
@ -98,6 +105,10 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
cpu: 1,
|
||||
ram: 12540,
|
||||
podTemplate: {
|
||||
podMetadata: {
|
||||
labels: ['label1', 'label2'],
|
||||
annotations: ['annotation1', 'annotation2'],
|
||||
},
|
||||
volumes: {
|
||||
home: '/home',
|
||||
data: [
|
||||
|
@ -145,6 +156,20 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
lastActivity: 'Last Activity',
|
||||
};
|
||||
|
||||
// Selected workspace
|
||||
const [selectedWorkspace, setSelectedWorkspace] = React.useState<Workspace | null>(null);
|
||||
|
||||
const selectWorkspace = React.useCallback(
|
||||
(newSelectedWorkspace) => {
|
||||
if (selectedWorkspace?.name === newSelectedWorkspace?.name) {
|
||||
setSelectedWorkspace(null);
|
||||
} else {
|
||||
setSelectedWorkspace(newSelectedWorkspace);
|
||||
}
|
||||
},
|
||||
[selectedWorkspace],
|
||||
);
|
||||
|
||||
// Filter
|
||||
const [activeAttributeMenu, setActiveAttributeMenu] = React.useState<string>(columnNames.name);
|
||||
const [isAttributeMenuOpen, setIsAttributeMenuOpen] = React.useState(false);
|
||||
|
@ -389,28 +414,51 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
|
||||
// Actions
|
||||
|
||||
const defaultActions = (workspace: Workspace): IActions =>
|
||||
[
|
||||
{
|
||||
title: 'Edit',
|
||||
onClick: () => console.log(`Clicked on edit, on row ${workspace.name}`),
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
onClick: () => console.log(`Clicked on delete, on row ${workspace.name}`),
|
||||
},
|
||||
{
|
||||
isSeparator: true,
|
||||
},
|
||||
{
|
||||
title: 'Start/restart',
|
||||
onClick: () => console.log(`Clicked on start/restart, on row ${workspace.name}`),
|
||||
},
|
||||
{
|
||||
title: 'Stop',
|
||||
onClick: () => console.log(`Clicked on stop, on row ${workspace.name}`),
|
||||
},
|
||||
] as IActions;
|
||||
const editAction = React.useCallback((workspace: Workspace) => {
|
||||
console.log(`Clicked on edit, on row ${workspace.name}`);
|
||||
}, []);
|
||||
|
||||
const deleteAction = React.useCallback((workspace: Workspace) => {
|
||||
console.log(`Clicked on delete, on row ${workspace.name}`);
|
||||
}, []);
|
||||
|
||||
const startRestartAction = React.useCallback((workspace: Workspace) => {
|
||||
console.log(`Clicked on start/restart, on row ${workspace.name}`);
|
||||
}, []);
|
||||
|
||||
const stopAction = React.useCallback((workspace: Workspace) => {
|
||||
console.log(`Clicked on stop, on row ${workspace.name}`);
|
||||
}, []);
|
||||
|
||||
const defaultActions = React.useCallback(
|
||||
(workspace: Workspace): IActions =>
|
||||
[
|
||||
{
|
||||
title: 'View Details',
|
||||
onClick: () => selectWorkspace(workspace),
|
||||
},
|
||||
{
|
||||
title: 'Edit',
|
||||
onClick: () => editAction(workspace),
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
onClick: () => deleteAction(workspace),
|
||||
},
|
||||
{
|
||||
isSeparator: true,
|
||||
},
|
||||
{
|
||||
title: 'Start/restart',
|
||||
onClick: () => startRestartAction(workspace),
|
||||
},
|
||||
{
|
||||
title: 'Stop',
|
||||
onClick: () => stopAction(workspace),
|
||||
},
|
||||
] as IActions,
|
||||
[selectWorkspace, editAction, deleteAction, startRestartAction, stopAction],
|
||||
);
|
||||
|
||||
// States
|
||||
|
||||
|
@ -447,80 +495,100 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
setPage(newPage);
|
||||
};
|
||||
|
||||
const workspaceDetailsContent = (
|
||||
<>
|
||||
{selectedWorkspace && (
|
||||
<WorkspaceDetails
|
||||
workspace={selectedWorkspace}
|
||||
onCloseClick={() => selectWorkspace(null)}
|
||||
onEditClick={() => editAction(selectedWorkspace)}
|
||||
onDeleteClick={() => deleteAction(selectedWorkspace)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Title headingLevel="h1">Kubeflow Workspaces</Title>
|
||||
<p>View your existing workspaces or create new workspaces.</p>
|
||||
{toolbar}
|
||||
<Table aria-label="Sortable table" ouiaId="SortableTable">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th sort={getSortParams(0)}>{columnNames.name}</Th>
|
||||
<Th sort={getSortParams(1)}>{columnNames.kind}</Th>
|
||||
<Th sort={getSortParams(2)}>{columnNames.image}</Th>
|
||||
<Th sort={getSortParams(3)}>{columnNames.podConfig}</Th>
|
||||
<Th sort={getSortParams(4)}>{columnNames.state}</Th>
|
||||
<Th sort={getSortParams(5)}>{columnNames.homeVol}</Th>
|
||||
<Th sort={getSortParams(6)} info={{ tooltip: 'Workspace CPU usage' }}>
|
||||
{columnNames.cpu}
|
||||
</Th>
|
||||
<Th sort={getSortParams(7)} info={{ tooltip: 'Workspace memory usage' }}>
|
||||
{columnNames.ram}
|
||||
</Th>
|
||||
<Th sort={getSortParams(8)}>{columnNames.lastActivity}</Th>
|
||||
<Th screenReaderText="Primary action" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
{sortedWorkspaces.map((workspace, rowIndex) => (
|
||||
<Tbody key={rowIndex} isExpanded={isWorkspaceExpanded(workspace)}>
|
||||
<Tr>
|
||||
<Td
|
||||
expand={{
|
||||
rowIndex,
|
||||
isExpanded: isWorkspaceExpanded(workspace),
|
||||
onToggle: () => setWorkspaceExpanded(workspace, !isWorkspaceExpanded(workspace)),
|
||||
}}
|
||||
/>
|
||||
<Td dataLabel={columnNames.name}>{workspace.name}</Td>
|
||||
<Td dataLabel={columnNames.kind}>{workspace.kind}</Td>
|
||||
<Td dataLabel={columnNames.image}>{workspace.options.imageConfig}</Td>
|
||||
<Td dataLabel={columnNames.podConfig}>{workspace.options.podConfig}</Td>
|
||||
<Td dataLabel={columnNames.state}>
|
||||
<Label color={stateColors[workspace.status.state]}>
|
||||
{WorkspaceState[workspace.status.state]}
|
||||
</Label>
|
||||
</Td>
|
||||
<Td dataLabel={columnNames.homeVol}>{workspace.podTemplate.volumes.home}</Td>
|
||||
<Td dataLabel={columnNames.cpu}>{`${workspace.cpu}%`}</Td>
|
||||
<Td dataLabel={columnNames.ram}>{formatRam(workspace.ram)}</Td>
|
||||
<Td dataLabel={columnNames.lastActivity}>
|
||||
<Timestamp
|
||||
date={new Date(workspace.status.activity.lastActivity)}
|
||||
tooltip={{ variant: TimestampTooltipVariant.default }}
|
||||
>
|
||||
1 hour ago
|
||||
</Timestamp>
|
||||
</Td>
|
||||
<Td isActionCell>
|
||||
<ActionsColumn items={defaultActions(workspace)} />
|
||||
</Td>
|
||||
</Tr>
|
||||
{isWorkspaceExpanded(workspace) && (
|
||||
<ExpandedWorkspaceRow workspace={workspace} columnNames={columnNames} />
|
||||
)}
|
||||
</Tbody>
|
||||
))}
|
||||
</Table>
|
||||
<Pagination
|
||||
itemCount={333}
|
||||
widgetId="bottom-example"
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
variant={PaginationVariant.bottom}
|
||||
onSetPage={onSetPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
/>
|
||||
</PageSection>
|
||||
<Drawer isExpanded={selectedWorkspace != null}>
|
||||
<DrawerContent panelContent={workspaceDetailsContent}>
|
||||
<DrawerContentBody>
|
||||
<PageSection isFilled>
|
||||
<Title headingLevel="h1">Kubeflow Workspaces</Title>
|
||||
<p>View your existing workspaces or create new workspaces.</p>
|
||||
{toolbar}
|
||||
<Table aria-label="Sortable table" ouiaId="SortableTable">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th sort={getSortParams(0)}>{columnNames.name}</Th>
|
||||
<Th sort={getSortParams(1)}>{columnNames.kind}</Th>
|
||||
<Th sort={getSortParams(2)}>{columnNames.image}</Th>
|
||||
<Th sort={getSortParams(3)}>{columnNames.podConfig}</Th>
|
||||
<Th sort={getSortParams(4)}>{columnNames.state}</Th>
|
||||
<Th sort={getSortParams(5)}>{columnNames.homeVol}</Th>
|
||||
<Th sort={getSortParams(6)} info={{ tooltip: 'Workspace CPU usage' }}>
|
||||
{columnNames.cpu}
|
||||
</Th>
|
||||
<Th sort={getSortParams(7)} info={{ tooltip: 'Workspace memory usage' }}>
|
||||
{columnNames.ram}
|
||||
</Th>
|
||||
<Th sort={getSortParams(8)}>{columnNames.lastActivity}</Th>
|
||||
<Th screenReaderText="Primary action" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
{sortedWorkspaces.map((workspace, rowIndex) => (
|
||||
<Tbody key={rowIndex} isExpanded={isWorkspaceExpanded(workspace)}>
|
||||
<Tr>
|
||||
<Td
|
||||
expand={{
|
||||
rowIndex,
|
||||
isExpanded: isWorkspaceExpanded(workspace),
|
||||
onToggle: () =>
|
||||
setWorkspaceExpanded(workspace, !isWorkspaceExpanded(workspace)),
|
||||
}}
|
||||
/>
|
||||
<Td dataLabel={columnNames.name}>{workspace.name}</Td>
|
||||
<Td dataLabel={columnNames.kind}>{workspace.kind}</Td>
|
||||
<Td dataLabel={columnNames.image}>{workspace.options.imageConfig}</Td>
|
||||
<Td dataLabel={columnNames.podConfig}>{workspace.options.podConfig}</Td>
|
||||
<Td dataLabel={columnNames.state}>
|
||||
<Label color={stateColors[workspace.status.state]}>
|
||||
{WorkspaceState[workspace.status.state]}
|
||||
</Label>
|
||||
</Td>
|
||||
<Td dataLabel={columnNames.homeVol}>{workspace.podTemplate.volumes.home}</Td>
|
||||
<Td dataLabel={columnNames.cpu}>{`${workspace.cpu}%`}</Td>
|
||||
<Td dataLabel={columnNames.ram}>{formatRam(workspace.ram)}</Td>
|
||||
<Td dataLabel={columnNames.lastActivity}>
|
||||
<Timestamp
|
||||
date={new Date(workspace.status.activity.lastActivity)}
|
||||
tooltip={{ variant: TimestampTooltipVariant.default }}
|
||||
>
|
||||
1 hour ago
|
||||
</Timestamp>
|
||||
</Td>
|
||||
<Td isActionCell>
|
||||
<ActionsColumn items={defaultActions(workspace)} />
|
||||
</Td>
|
||||
</Tr>
|
||||
{isWorkspaceExpanded(workspace) && (
|
||||
<ExpandedWorkspaceRow workspace={workspace} columnNames={columnNames} />
|
||||
)}
|
||||
</Tbody>
|
||||
))}
|
||||
</Table>
|
||||
<Pagination
|
||||
itemCount={333}
|
||||
widgetId="bottom-example"
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
variant={PaginationVariant.bottom}
|
||||
onSetPage={onSetPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
/>
|
||||
</PageSection>
|
||||
</DrawerContentBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -55,6 +55,10 @@ export interface Workspace {
|
|||
cpu: number;
|
||||
ram: number;
|
||||
podTemplate: {
|
||||
podMetadata: {
|
||||
labels: string[];
|
||||
annotations: string[];
|
||||
};
|
||||
volumes: {
|
||||
home: string;
|
||||
data: {
|
||||
|
|
Loading…
Reference in New Issue