feat(ws): Make Workspace Kind drawer resizable and add table view to WS kind details (#483)

Signed-off-by: Charles Thao <cthao@redhat.com>
This commit is contained in:
Charles Thao 2025-07-18 15:29:41 -04:00 committed by Bhakti Narvekar
parent 13a66aef2b
commit de0e5c4356
5 changed files with 156 additions and 114 deletions

View File

@ -45,7 +45,7 @@ export const WorkspaceKindDetails: React.FunctionComponent<WorkspaceKindDetailsP
};
return (
<DrawerPanelContent data-testid="workspaceDetails">
<DrawerPanelContent minSize="45%" isResizable data-testid="workspaceDetails">
<DrawerHead>
<Title headingLevel="h6">{workspaceKind.name}</Title>
<DrawerActions>

View File

@ -1,8 +1,7 @@
import React from 'react';
import { Button, List, ListItem } from '@patternfly/react-core';
import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
import { useTypedNavigate } from '~/app/routerHelper';
import { WorkspaceKindDetailsTable } from './WorkspaceKindDetailsTable';
type WorkspaceDetailsImagesProps = {
workspaceKind: WorkspaceKind;
@ -12,37 +11,21 @@ type WorkspaceDetailsImagesProps = {
export const WorkspaceKindDetailsImages: React.FunctionComponent<WorkspaceDetailsImagesProps> = ({
workspaceKind,
workspaceCountPerKind,
}) => {
const navigate = useTypedNavigate();
return (
<List isPlain>
{workspaceKind.podTemplate.options.imageConfig.values.map((image, rowIndex) => (
<ListItem key={rowIndex}>
{image.displayName}:{' '}
<Button
variant="link"
isInline
className="workspace-kind-summary-button"
onClick={() =>
navigate('workspaceKindSummary', {
params: { kind: workspaceKind.name },
state: {
}) => (
<WorkspaceKindDetailsTable
rows={workspaceKind.podTemplate.options.imageConfig.values.map((image) => ({
id: image.id,
displayName: image.displayName,
kindName: workspaceKind.name,
workspaceCountRouteState: {
imageId: image.id,
},
})
}
>
{
workspaceCount:
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByImage[image.id] ?? 0
: 0
}
{' Workspaces'}
</Button>
</ListItem>
))}
</List>
: 0,
}))}
tableKind="image"
/>
);
};

View File

@ -1,8 +1,7 @@
import React from 'react';
import { Button, List, ListItem } from '@patternfly/react-core';
import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
import { useTypedNavigate } from '~/app/routerHelper';
import { WorkspaceKindDetailsTable } from './WorkspaceKindDetailsTable';
type WorkspaceDetailsNamespacesProps = {
workspaceKind: WorkspaceKind;
@ -11,42 +10,25 @@ type WorkspaceDetailsNamespacesProps = {
export const WorkspaceKindDetailsNamespaces: React.FunctionComponent<
WorkspaceDetailsNamespacesProps
> = ({ workspaceKind, workspaceCountPerKind }) => {
const navigate = useTypedNavigate();
return (
<List isPlain>
{Object.keys(
> = ({ workspaceKind, workspaceCountPerKind }) => (
<WorkspaceKindDetailsTable
rows={Object.keys(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByNamespace
: [],
).map((namespace, rowIndex) => (
<ListItem key={rowIndex}>
{namespace}:{' '}
<Button
variant="link"
className="workspace-kind-summary-button"
isInline
onClick={() =>
navigate('workspaceKindSummary', {
params: { kind: workspaceKind.name },
state: {
).map((namespace, rowIndex) => ({
id: String(rowIndex),
displayName: namespace,
kindName: workspaceKind.name,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCount: workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByNamespace[namespace]
: 0,
workspaceCountRouteState: {
namespace,
},
})
}
>
{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByNamespace[namespace]
: 0
}
{' Workspaces'}
</Button>
</ListItem>
))}
</List>
}))}
tableKind="namespace"
/>
);
};

View File

@ -1,8 +1,7 @@
import React from 'react';
import { Button, List, ListItem } from '@patternfly/react-core';
import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
import { useTypedNavigate } from '~/app/routerHelper';
import { WorkspaceKindDetailsTable } from './WorkspaceKindDetailsTable';
type WorkspaceDetailsPodConfigsProps = {
workspaceKind: WorkspaceKind;
@ -11,37 +10,20 @@ type WorkspaceDetailsPodConfigsProps = {
export const WorkspaceKindDetailsPodConfigs: React.FunctionComponent<
WorkspaceDetailsPodConfigsProps
> = ({ workspaceKind, workspaceCountPerKind }) => {
const navigate = useTypedNavigate();
return (
<List isPlain>
{workspaceKind.podTemplate.options.podConfig.values.map((podConfig, rowIndex) => (
<ListItem key={rowIndex}>
{podConfig.displayName}:{' '}
<Button
variant="link"
className="workspace-kind-summary-button"
isInline
onClick={() =>
navigate('workspaceKindSummary', {
params: { kind: workspaceKind.name },
state: {
> = ({ workspaceKind, workspaceCountPerKind }) => (
<WorkspaceKindDetailsTable
rows={workspaceKind.podTemplate.options.podConfig.values.map((podConfig) => ({
id: podConfig.id,
displayName: podConfig.displayName,
kindName: workspaceKind.name,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCount: workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByPodConfig[podConfig.id] ?? 0
: 0,
workspaceCountRouteState: {
podConfigId: podConfig.id,
},
})
}
>
{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByPodConfig[podConfig.id] ?? 0
: 0
}
{' Workspaces'}
</Button>
</ListItem>
))}
</List>
}))}
tableKind="podConfig"
/>
);
};

View File

@ -0,0 +1,95 @@
import React, { useMemo, useState } from 'react';
import { Table, Thead, Tr, Td, Tbody, Th } from '@patternfly/react-table';
import { Button, Content, Pagination, PaginationVariant } from '@patternfly/react-core';
import { useTypedNavigate } from '~/app/routerHelper';
import { RouteStateMap } from '~/app/routes';
export interface WorkspaceKindDetailsTableRow {
id: string;
displayName: string;
kindName: string;
workspaceCount: number;
workspaceCountRouteState: RouteStateMap['workspaceKindSummary'];
}
interface WorkspaceKindDetailsTableProps {
rows: WorkspaceKindDetailsTableRow[];
tableKind: 'image' | 'podConfig' | 'namespace';
}
export const WorkspaceKindDetailsTable: React.FC<WorkspaceKindDetailsTableProps> = ({
rows,
tableKind,
}) => {
const navigate = useTypedNavigate();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const rowPages = useMemo(() => {
const pages = [];
for (let i = 0; i < rows.length; i += perPage) {
pages.push(rows.slice(i, i + perPage));
}
return pages;
}, [perPage, rows]);
const onSetPage = (
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
newPage: number,
) => {
setPage(newPage);
};
const onPerPageSelect = (
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
newPerPage: number,
newPage: number,
) => {
setPerPage(newPerPage);
setPage(newPage);
};
return (
<Content>
<Table aria-label={`workspace-kind-details-${tableKind}`}>
<Thead>
<Tr>
<Th>Name</Th>
<Th>Workspaces</Th>
</Tr>
</Thead>
<Tbody>
{rowPages[page - 1].map((row) => (
<Tr key={row.id}>
<Td>{row.displayName}</Td>
<Td>
<Button
variant="link"
isInline
className="workspace-kind-summary-button"
onClick={() =>
navigate('workspaceKindSummary', {
params: { kind: row.kindName },
state: row.workspaceCountRouteState,
})
}
>
{row.workspaceCount} Workspaces
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Pagination
itemCount={rows.length}
widgetId="pagination-bottom"
perPage={perPage}
page={page}
variant={PaginationVariant.bottom}
isCompact
onSetPage={onSetPage}
onPerPageSelect={onPerPageSelect}
/>
</Content>
);
};