Skip to content

Commit 98c71ed

Browse files
committed
feat(ws): Workspace list: Selectable rows
Signed-off-by: paulovmr <[email protected]>
1 parent 7c660e4 commit 98c71ed

File tree

5 files changed

+165
-72
lines changed

5 files changed

+165
-72
lines changed

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/WorkspaceDetailsActivity.cy.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@ describe('WorkspaceDetailsActivity Component', () => {
1212

1313
// This tests depends on the mocked workspaces data at home page, needs revisit once workspace data fetched from BE
1414
it('open workspace details, open activity tab, check all fields match', () => {
15-
cy.findAllByTestId('table-body').first().findByTestId('action-column').click();
15+
cy.findAllByTestId('table-body').first().findByTestId('workspace-select').find('input').click();
1616
// Extract first workspace from mock data
1717
cy.wait('@getWorkspaces').then((interception) => {
1818
if (!interception.response || !interception.response.body) {
1919
throw new Error('Intercepted response is undefined or empty');
2020
}
2121
const workspace = interception.response.body.data[0];
22-
cy.findByTestId('action-view-details').click();
2322
cy.findByTestId('activityTab').click();
2423
cy.findByTestId('lastActivity')
2524
.invoke('text')
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import {
3+
Button,
4+
DrawerActions,
5+
DrawerHead,
6+
DrawerPanelBody,
7+
DrawerPanelContent,
8+
Title,
9+
} from '@patternfly/react-core';
10+
import { WorkspaceAggregatedDetailsActions } from '~/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetailsActions';
11+
12+
type WorkspaceAggregatedDetailsProps = {
13+
workspaceNames: string[];
14+
onCloseClick: React.MouseEventHandler;
15+
onDeleteClick: React.MouseEventHandler;
16+
};
17+
18+
export const WorkspaceAggregatedDetails: React.FunctionComponent<
19+
WorkspaceAggregatedDetailsProps
20+
> = ({ workspaceNames, onCloseClick, onDeleteClick }) => (
21+
<DrawerPanelContent>
22+
<DrawerHead>
23+
<Title headingLevel="h6">Multiple selected workspaces</Title>
24+
<WorkspaceAggregatedDetailsActions onDeleteClick={onDeleteClick} />
25+
<DrawerActions>
26+
<Button onClick={onCloseClick} aria-label="Clear workspoaces selection" variant="link">
27+
Clear selection
28+
</Button>
29+
</DrawerActions>
30+
</DrawerHead>
31+
<DrawerPanelBody>
32+
<Title headingLevel="h6" size="md">
33+
{'Selected workspaces: '}
34+
</Title>
35+
{workspaceNames.join(', ')}
36+
</DrawerPanelBody>
37+
</DrawerPanelContent>
38+
);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as React from 'react';
2+
import {
3+
Dropdown,
4+
DropdownList,
5+
MenuToggle,
6+
DropdownItem,
7+
Flex,
8+
FlexItem,
9+
} from '@patternfly/react-core';
10+
11+
interface WorkspaceAggregatedDetailsActionsProps {
12+
onDeleteClick: React.MouseEventHandler;
13+
}
14+
15+
export const WorkspaceAggregatedDetailsActions: React.FC<
16+
WorkspaceAggregatedDetailsActionsProps
17+
> = ({ onDeleteClick }) => {
18+
const [isOpen, setOpen] = React.useState(false);
19+
20+
return (
21+
<Flex>
22+
<FlexItem>
23+
<Dropdown
24+
isOpen={isOpen}
25+
onSelect={() => setOpen(false)}
26+
onOpenChange={(open) => setOpen(open)}
27+
popperProps={{ position: 'end' }}
28+
toggle={(toggleRef) => (
29+
<MenuToggle
30+
variant="primary"
31+
ref={toggleRef}
32+
onClick={() => setOpen(!isOpen)}
33+
isExpanded={isOpen}
34+
aria-label="Workspace aggregated details action toggle"
35+
data-testid="workspace-aggregated-details-action-toggle"
36+
>
37+
Actions
38+
</MenuToggle>
39+
)}
40+
>
41+
<DropdownList>
42+
<DropdownItem
43+
id="workspace-aggregated-details-action-delete-button"
44+
aria-label="Delete selected workspace"
45+
key="delete-aggregated-workspace-button"
46+
onClick={onDeleteClick}
47+
>
48+
Delete selected
49+
</DropdownItem>
50+
</DropdownList>
51+
</Dropdown>
52+
</FlexItem>
53+
</Flex>
54+
);
55+
};

workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,19 @@ import * as React from 'react';
22
import { ExpandableRowContent, Td, Tr } from '@patternfly/react-table';
33
import { Workspace } from '~/shared/api/backendApiTypes';
44
import { DataVolumesList } from '~/app/pages/Workspaces/DataVolumesList';
5-
import { WorkspacesColumnNames } from '~/app/types';
65

76
interface ExpandedWorkspaceRowProps {
87
workspace: Workspace;
9-
columnNames: WorkspacesColumnNames;
108
}
119

12-
export const ExpandedWorkspaceRow: React.FC<ExpandedWorkspaceRowProps> = ({
13-
workspace,
14-
columnNames,
15-
}) => {
16-
const renderExpandedData = () =>
17-
Object.keys(columnNames).map((colName, index) => {
18-
switch (colName) {
19-
case 'name':
20-
return (
21-
<Td noPadding colSpan={1} key={index}>
22-
<ExpandableRowContent>
23-
<DataVolumesList workspace={workspace} />
24-
</ExpandableRowContent>
25-
</Td>
26-
);
27-
default:
28-
return <Td key={index} />;
29-
}
30-
});
31-
32-
return (
33-
<Tr>
34-
<Td />
35-
{renderExpandedData()}
36-
</Tr>
37-
);
38-
};
10+
export const ExpandedWorkspaceRow: React.FC<ExpandedWorkspaceRowProps> = ({ workspace }) => (
11+
<Tr>
12+
<Td colSpan={3} />
13+
<Td noPadding colSpan={3}>
14+
<ExpandableRowContent>
15+
<DataVolumesList workspace={workspace} />
16+
</ExpandableRowContent>
17+
</Td>
18+
<Td colSpan={7} />
19+
</Tr>
20+
);

workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ import { WorkspacesColumnNames } from '~/app/types';
5656
import CustomEmptyState from '~/shared/components/CustomEmptyState';
5757
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
5858
import { extractCpuValue, extractMemoryValue } from '~/shared/utilities/WorkspaceUtils';
59+
import { WorkspaceAggregatedDetails } from '~/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetails';
5960

6061
export enum ActionType {
61-
ViewDetails,
6262
Edit,
6363
Delete,
6464
Start,
@@ -139,17 +139,6 @@ export const Workspaces: React.FunctionComponent = () => {
139139
});
140140
}, [activeActionType, navigate, selectedWorkspace]);
141141

142-
const selectWorkspace = React.useCallback(
143-
(newSelectedWorkspace: Workspace | null) => {
144-
if (selectedWorkspace?.name === newSelectedWorkspace?.name) {
145-
setSelectedWorkspace(null);
146-
} else {
147-
setSelectedWorkspace(newSelectedWorkspace);
148-
}
149-
},
150-
[selectedWorkspace],
151-
);
152-
153142
const setWorkspaceExpanded = (workspace: Workspace, isExpanding = true) =>
154143
setExpandedWorkspacesNames((prevExpanded) => {
155144
const newExpandedWorkspacesNames = prevExpanded.filter((wsName) => wsName !== workspace.name);
@@ -274,11 +263,6 @@ export const Workspaces: React.FunctionComponent = () => {
274263

275264
// Actions
276265

277-
const viewDetailsClick = React.useCallback((workspace: Workspace) => {
278-
setSelectedWorkspace(workspace);
279-
setActiveActionType(ActionType.ViewDetails);
280-
}, []);
281-
282266
// TODO: Uncomment when edit action is fully supported
283267
// const editAction = React.useCallback((workspace: Workspace) => {
284268
// setSelectedWorkspace(workspace);
@@ -328,11 +312,6 @@ export const Workspaces: React.FunctionComponent = () => {
328312

329313
const workspaceDefaultActions = (workspace: Workspace): IActions => {
330314
const workspaceActions = [
331-
{
332-
id: 'view-details',
333-
title: 'View Details',
334-
onClick: () => viewDetailsClick(workspace),
335-
},
336315
// TODO: Uncomment when edit action is fully supported
337316
// {
338317
// id: 'edit',
@@ -501,26 +480,50 @@ export const Workspaces: React.FunctionComponent = () => {
501480
setPage(newPage);
502481
};
503482

504-
const workspaceDetailsContent = (
505-
<>
506-
{selectedWorkspace && (
507-
<WorkspaceDetails
508-
workspace={selectedWorkspace}
509-
onCloseClick={() => selectWorkspace(null)}
510-
// TODO: Uncomment when edit action is fully supported
511-
// onEditClick={() => editAction(selectedWorkspace)}
512-
onDeleteClick={() => handleDeleteClick(selectedWorkspace)}
513-
/>
514-
)}
515-
</>
516-
);
483+
const [selectedWorkspaceNames, setSelectedWorkspaceNames] = React.useState<string[]>([]);
484+
const setWorkspaceSelected = (workspace: Workspace, isSelecting = true) =>
485+
setSelectedWorkspaceNames((prevSelected) => {
486+
const otherSelectedWorkspaceNames = prevSelected.filter((w) => w !== workspace.name);
487+
return isSelecting
488+
? [...otherSelectedWorkspaceNames, workspace.name]
489+
: otherSelectedWorkspaceNames;
490+
});
491+
const selectAllWorkspaces = (isSelecting = true) =>
492+
setSelectedWorkspaceNames(isSelecting ? sortedWorkspaces.map((r) => r.name) : []);
493+
const areAllWorkspacesSelected = selectedWorkspaceNames.length === sortedWorkspaces.length;
494+
const isWorkspaceSelected = (workspace: Workspace) =>
495+
selectedWorkspaceNames.includes(workspace.name);
496+
497+
const workspaceDetailsContent = () => {
498+
const selectedWorkspaceForDetails =
499+
selectedWorkspaceNames.length === 1
500+
? sortedWorkspaces.find((w) => w.name === selectedWorkspaceNames[0])
501+
: undefined;
502+
return (
503+
<>
504+
{selectedWorkspaceForDetails && (
505+
<WorkspaceDetails
506+
workspace={selectedWorkspaceForDetails}
507+
onCloseClick={() => selectAllWorkspaces(false)}
508+
// TODO: Uncomment when edit action is fully supported
509+
// onEditClick={() => editAction(selectedWorkspaceForDetails)}
510+
onDeleteClick={() => handleDeleteClick(selectedWorkspaceForDetails)}
511+
/>
512+
)}
513+
{selectedWorkspaceNames.length > 1 && (
514+
<WorkspaceAggregatedDetails
515+
workspaceNames={selectedWorkspaceNames}
516+
onCloseClick={() => selectAllWorkspaces(false)}
517+
onDeleteClick={() => console.log('Delete selected workspaces')}
518+
/>
519+
)}
520+
</>
521+
);
522+
};
517523

518524
return (
519-
<Drawer
520-
isInline
521-
isExpanded={selectedWorkspace != null && activeActionType === ActionType.ViewDetails}
522-
>
523-
<DrawerContent panelContent={workspaceDetailsContent}>
525+
<Drawer isExpanded={selectedWorkspaceNames.length >= 1}>
526+
<DrawerContent panelContent={workspaceDetailsContent()}>
524527
<DrawerContentBody>
525528
<PageSection isFilled>
526529
<Content>
@@ -545,6 +548,13 @@ export const Workspaces: React.FunctionComponent = () => {
545548
<Thead>
546549
<Tr>
547550
<Th screenReaderText="expand-action" />
551+
<Th
552+
select={{
553+
onSelect: (_event, isSelecting) => selectAllWorkspaces(isSelecting),
554+
isSelected: areAllWorkspacesSelected,
555+
}}
556+
aria-label="Row select"
557+
/>
548558
{Object.values(columnNames).map((columnName, index) => (
549559
<Th
550560
key={`${columnName}-col-name`}
@@ -576,6 +586,15 @@ export const Workspaces: React.FunctionComponent = () => {
576586
setWorkspaceExpanded(workspace, !isWorkspaceExpanded(workspace)),
577587
}}
578588
/>
589+
<Td
590+
select={{
591+
rowIndex,
592+
onSelect: (_event, isSelecting) =>
593+
setWorkspaceSelected(workspace, isSelecting),
594+
isSelected: isWorkspaceSelected(workspace),
595+
}}
596+
data-testid="workspace-select"
597+
/>
579598
<Td dataLabel={columnNames.redirectStatus}>
580599
{workspaceRedirectStatus[workspace.workspaceKind.name]
581600
? getRedirectStatusIcon(
@@ -640,7 +659,7 @@ export const Workspaces: React.FunctionComponent = () => {
640659
</Td>
641660
</Tr>
642661
{isWorkspaceExpanded(workspace) && (
643-
<ExpandedWorkspaceRow workspace={workspace} columnNames={columnNames} />
662+
<ExpandedWorkspaceRow workspace={workspace} />
644663
)}
645664
</Tbody>
646665
))}

0 commit comments

Comments
 (0)