From 98c71edb73ff1063bb74318eeda4fbcc1fdfa3f2 Mon Sep 17 00:00:00 2001 From: paulovmr <832830+paulovmr@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:45:10 -0300 Subject: [PATCH] feat(ws): Workspace list: Selectable rows Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com> --- .../workspaces/WorkspaceDetailsActivity.cy.ts | 3 +- .../WorkspaceAggregatedDetails.tsx | 38 +++++++ .../WorkspaceAggregatedDetailsActions.tsx | 55 ++++++++++ .../pages/Workspaces/ExpandedWorkspaceRow.tsx | 40 ++----- .../src/app/pages/Workspaces/Workspaces.tsx | 101 +++++++++++------- 5 files changed, 165 insertions(+), 72 deletions(-) create mode 100644 workspaces/frontend/src/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetails.tsx create mode 100644 workspaces/frontend/src/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetailsActions.tsx diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/WorkspaceDetailsActivity.cy.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/WorkspaceDetailsActivity.cy.ts index 06307a5eb..db4167c6e 100644 --- a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/WorkspaceDetailsActivity.cy.ts +++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/WorkspaceDetailsActivity.cy.ts @@ -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') diff --git a/workspaces/frontend/src/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetails.tsx new file mode 100644 index 000000000..b9019d768 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetails.tsx @@ -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 }) => ( + + + Multiple selected workspaces + + + + + + + + {'Selected workspaces: '} + + {workspaceNames.join(', ')} + + +); diff --git a/workspaces/frontend/src/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetailsActions.tsx b/workspaces/frontend/src/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetailsActions.tsx new file mode 100644 index 000000000..40f711f13 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/DetailsAggregated/WorkspaceAggregatedDetailsActions.tsx @@ -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 ( + + + setOpen(false)} + onOpenChange={(open) => setOpen(open)} + popperProps={{ position: 'end' }} + toggle={(toggleRef) => ( + setOpen(!isOpen)} + isExpanded={isOpen} + aria-label="Workspace aggregated details action toggle" + data-testid="workspace-aggregated-details-action-toggle" + > + Actions + + )} + > + + + Delete selected + + + + + + ); +}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx b/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx index 5fd2e38c2..43d494060 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx @@ -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 = ({ - workspace, - columnNames, -}) => { - const renderExpandedData = () => - Object.keys(columnNames).map((colName, index) => { - switch (colName) { - case 'name': - return ( - - - - - - ); - default: - return ; - } - }); - - return ( - - - {renderExpandedData()} - - ); -}; +export const ExpandedWorkspaceRow: React.FC = ({ workspace }) => ( + + + + + + + + + +); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index 7036486bf..67f541370 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -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 && ( - selectWorkspace(null)} - // TODO: Uncomment when edit action is fully supported - // onEditClick={() => editAction(selectedWorkspace)} - onDeleteClick={() => handleDeleteClick(selectedWorkspace)} - /> - )} - - ); + const [selectedWorkspaceNames, setSelectedWorkspaceNames] = React.useState([]); + 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 && ( + selectAllWorkspaces(false)} + // TODO: Uncomment when edit action is fully supported + // onEditClick={() => editAction(selectedWorkspaceForDetails)} + onDeleteClick={() => handleDeleteClick(selectedWorkspaceForDetails)} + /> + )} + {selectedWorkspaceNames.length > 1 && ( + selectAllWorkspaces(false)} + onDeleteClick={() => console.log('Delete selected workspaces')} + /> + )} + + ); + }; return ( - - + = 1}> + @@ -545,6 +548,13 @@ export const Workspaces: React.FunctionComponent = () => { + selectAllWorkspaces(isSelecting), + isSelected: areAllWorkspacesSelected, + }} + aria-label="Row select" + /> {Object.values(columnNames).map((columnName, index) => ( { setWorkspaceExpanded(workspace, !isWorkspaceExpanded(workspace)), }} /> + + setWorkspaceSelected(workspace, isSelecting), + isSelected: isWorkspaceSelected(workspace), + }} + data-testid="workspace-select" + /> {workspaceRedirectStatus[workspace.workspaceKind.name] ? getRedirectStatusIcon( @@ -640,7 +659,7 @@ export const Workspaces: React.FunctionComponent = () => { {isWorkspaceExpanded(workspace) && ( - + )} ))}