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:
Paulo Rego 2025-01-13 10:33:08 -03:00 committed by GitHub
parent 249a45677d
commit d84621aac1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 342 additions and 97 deletions

View File

@ -43,6 +43,7 @@ const App: React.FC = () => {
<Page
mainContainerId="primary-app-container"
masthead={masthead}
isContentFilled
isManagedSidebar
sidebar={<NavSidebar />}
>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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>
);
};

View File

@ -55,6 +55,10 @@ export interface Workspace {
cpu: number;
ram: number;
podTemplate: {
podMetadata: {
labels: string[];
annotations: string[];
};
volumes: {
home: string;
data: {