Skip to content

feat(ws): Workspace list: Selectable rows #196

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

Open
wants to merge 1 commit into
base: notebooks-v2
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -12,14 +12,13 @@ describe('WorkspaceDetailsActivity Component', () => {

// This tests depends on the mocked workspaces data at home page, needs revisit once workspace data fetched from BE
it('open workspace details, open activity tab, check all fields match', () => {
cy.findAllByTestId('table-body').first().findByTestId('action-column').click();
cy.findAllByTestId('table-body').first().findByTestId('workspace-select').find('input').click();
// Extract first workspace from mock data
cy.wait('@getWorkspaces').then((interception) => {
if (!interception.response || !interception.response.body) {
throw new Error('Intercepted response is undefined or empty');
}
const workspace = interception.response.body.data[0];
cy.findByTestId('action-view-details').click();
cy.findByTestId('activityTab').click();
cy.findByTestId('lastActivity')
.invoke('text')
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import {
Button,
DrawerActions,
DrawerHead,
DrawerPanelBody,
DrawerPanelContent,
Title,
} from '@patternfly/react-core';
import { WorkspaceAggregatedDetailsActions } from '~/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetailsActions';

type WorkspaceAggregatedDetailsProps = {
workspaceNames: string[];
onCloseClick: React.MouseEventHandler;
onDeleteClick: React.MouseEventHandler;
};

export const WorkspaceAggregatedDetails: React.FunctionComponent<
WorkspaceAggregatedDetailsProps
> = ({ workspaceNames, onCloseClick, onDeleteClick }) => (
<DrawerPanelContent>
<DrawerHead>
<Title headingLevel="h6">Multiple selected workspaces</Title>
<WorkspaceAggregatedDetailsActions onDeleteClick={onDeleteClick} />
<DrawerActions>
<Button onClick={onCloseClick} aria-label="Clear workspoaces selection" variant="link">
Clear selection
</Button>
</DrawerActions>
</DrawerHead>
<DrawerPanelBody>
<Title headingLevel="h6" size="md">
{'Selected workspaces: '}
</Title>
{workspaceNames.join(', ')}
</DrawerPanelBody>
</DrawerPanelContent>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from 'react';
import {
Dropdown,
DropdownList,
MenuToggle,
DropdownItem,
Flex,
FlexItem,
} from '@patternfly/react-core';

interface WorkspaceAggregatedDetailsActionsProps {
onDeleteClick: React.MouseEventHandler;
}

export const WorkspaceAggregatedDetailsActions: React.FC<
WorkspaceAggregatedDetailsActionsProps
> = ({ 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 aggregated details action toggle"
data-testid="workspace-aggregated-details-action-toggle"
>
Actions
</MenuToggle>
)}
>
<DropdownList>
<DropdownItem
id="workspace-aggregated-details-action-delete-button"
aria-label="Delete selected workspace"
key="delete-aggregated-workspace-button"
onClick={onDeleteClick}
>
Delete selected
</DropdownItem>
</DropdownList>
</Dropdown>
</FlexItem>
</Flex>
);
};
Original file line number Diff line number Diff line change
@@ -2,37 +2,19 @@ import * as React from 'react';
import { ExpandableRowContent, Td, Tr } from '@patternfly/react-table';
import { Workspace } from '~/shared/api/backendApiTypes';
import { DataVolumesList } from '~/app/pages/Workspaces/DataVolumesList';
import { WorkspacesColumnNames } from '~/app/types';

interface ExpandedWorkspaceRowProps {
workspace: Workspace;
columnNames: WorkspacesColumnNames;
}

export const ExpandedWorkspaceRow: React.FC<ExpandedWorkspaceRowProps> = ({
workspace,
columnNames,
}) => {
const renderExpandedData = () =>
Object.keys(columnNames).map((colName, index) => {
switch (colName) {
case 'name':
return (
<Td noPadding colSpan={1} key={index}>
<ExpandableRowContent>
<DataVolumesList workspace={workspace} />
</ExpandableRowContent>
</Td>
);
default:
return <Td key={index} />;
}
});

return (
<Tr>
<Td />
{renderExpandedData()}
</Tr>
);
};
export const ExpandedWorkspaceRow: React.FC<ExpandedWorkspaceRowProps> = ({ workspace }) => (
<Tr>
<Td colSpan={3} />
<Td noPadding colSpan={3}>
<ExpandableRowContent>
<DataVolumesList workspace={workspace} />
</ExpandableRowContent>
</Td>
<Td colSpan={7} />
</Tr>
);
101 changes: 60 additions & 41 deletions workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
@@ -56,9 +56,9 @@ import { WorkspacesColumnNames } from '~/app/types';
import CustomEmptyState from '~/shared/components/CustomEmptyState';
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
import { extractCpuValue, extractMemoryValue } from '~/shared/utilities/WorkspaceUtils';
import { WorkspaceAggregatedDetails } from '~/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetails';

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

const selectWorkspace = React.useCallback(
(newSelectedWorkspace: Workspace | null) => {
if (selectedWorkspace?.name === newSelectedWorkspace?.name) {
setSelectedWorkspace(null);
} else {
setSelectedWorkspace(newSelectedWorkspace);
}
},
[selectedWorkspace],
);

const setWorkspaceExpanded = (workspace: Workspace, isExpanding = true) =>
setExpandedWorkspacesNames((prevExpanded) => {
const newExpandedWorkspacesNames = prevExpanded.filter((wsName) => wsName !== workspace.name);
@@ -274,11 +263,6 @@ export const Workspaces: React.FunctionComponent = () => {

// Actions

const viewDetailsClick = React.useCallback((workspace: Workspace) => {
setSelectedWorkspace(workspace);
setActiveActionType(ActionType.ViewDetails);
}, []);

// TODO: Uncomment when edit action is fully supported
// const editAction = React.useCallback((workspace: Workspace) => {
// setSelectedWorkspace(workspace);
@@ -328,11 +312,6 @@ export const Workspaces: React.FunctionComponent = () => {

const workspaceDefaultActions = (workspace: Workspace): IActions => {
const workspaceActions = [
{
id: 'view-details',
title: 'View Details',
onClick: () => viewDetailsClick(workspace),
},
// TODO: Uncomment when edit action is fully supported
// {
// id: 'edit',
@@ -501,26 +480,50 @@ export const Workspaces: React.FunctionComponent = () => {
setPage(newPage);
};

const workspaceDetailsContent = (
<>
{selectedWorkspace && (
<WorkspaceDetails
workspace={selectedWorkspace}
onCloseClick={() => selectWorkspace(null)}
// TODO: Uncomment when edit action is fully supported
// onEditClick={() => editAction(selectedWorkspace)}
onDeleteClick={() => handleDeleteClick(selectedWorkspace)}
/>
)}
</>
);
const [selectedWorkspaceNames, setSelectedWorkspaceNames] = React.useState<string[]>([]);
const setWorkspaceSelected = (workspace: Workspace, isSelecting = true) =>
setSelectedWorkspaceNames((prevSelected) => {
const otherSelectedWorkspaceNames = prevSelected.filter((w) => w !== workspace.name);
return isSelecting
? [...otherSelectedWorkspaceNames, workspace.name]
: otherSelectedWorkspaceNames;
});
const selectAllWorkspaces = (isSelecting = true) =>
setSelectedWorkspaceNames(isSelecting ? sortedWorkspaces.map((r) => r.name) : []);
const areAllWorkspacesSelected = selectedWorkspaceNames.length === sortedWorkspaces.length;
const isWorkspaceSelected = (workspace: Workspace) =>
selectedWorkspaceNames.includes(workspace.name);

const workspaceDetailsContent = () => {
const selectedWorkspaceForDetails =
selectedWorkspaceNames.length === 1
? sortedWorkspaces.find((w) => w.name === selectedWorkspaceNames[0])
: undefined;
return (
<>
{selectedWorkspaceForDetails && (
<WorkspaceDetails
workspace={selectedWorkspaceForDetails}
onCloseClick={() => selectAllWorkspaces(false)}
// TODO: Uncomment when edit action is fully supported
// onEditClick={() => editAction(selectedWorkspaceForDetails)}
onDeleteClick={() => handleDeleteClick(selectedWorkspaceForDetails)}
/>
)}
{selectedWorkspaceNames.length > 1 && (
<WorkspaceAggregatedDetails
workspaceNames={selectedWorkspaceNames}
onCloseClick={() => selectAllWorkspaces(false)}
onDeleteClick={() => console.log('Delete selected workspaces')}
/>
)}
</>
);
};

return (
<Drawer
isInline
isExpanded={selectedWorkspace != null && activeActionType === ActionType.ViewDetails}
>
<DrawerContent panelContent={workspaceDetailsContent}>
<Drawer isExpanded={selectedWorkspaceNames.length >= 1}>
<DrawerContent panelContent={workspaceDetailsContent()}>
<DrawerContentBody>
<PageSection isFilled>
<Content>
@@ -545,6 +548,13 @@ export const Workspaces: React.FunctionComponent = () => {
<Thead>
<Tr>
<Th screenReaderText="expand-action" />
<Th
select={{
onSelect: (_event, isSelecting) => selectAllWorkspaces(isSelecting),
isSelected: areAllWorkspacesSelected,
}}
aria-label="Row select"
/>
{Object.values(columnNames).map((columnName, index) => (
<Th
key={`${columnName}-col-name`}
@@ -576,6 +586,15 @@ export const Workspaces: React.FunctionComponent = () => {
setWorkspaceExpanded(workspace, !isWorkspaceExpanded(workspace)),
}}
/>
<Td
select={{
rowIndex,
onSelect: (_event, isSelecting) =>
setWorkspaceSelected(workspace, isSelecting),
isSelected: isWorkspaceSelected(workspace),
}}
data-testid="workspace-select"
/>
<Td dataLabel={columnNames.redirectStatus}>
{workspaceRedirectStatus[workspace.workspaceKind.name]
? getRedirectStatusIcon(
@@ -640,7 +659,7 @@ export const Workspaces: React.FunctionComponent = () => {
</Td>
</Tr>
{isWorkspaceExpanded(workspace) && (
<ExpandedWorkspaceRow workspace={workspace} columnNames={columnNames} />
<ExpandedWorkspaceRow workspace={workspace} />
)}
</Tbody>
))}