From 61f580dc068fc9183bb3c973a54cb4ed23160db3 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:09:20 +0800 Subject: [PATCH] [Workspace] Update workspace list page table (#7640) (#7761) * the table basically works * the table basically works new * This time I achieve the functionality and apprearence of the workLists table * This time I achieve the functionality and apprearence of the workLists table, add tests * This time I achieve the functionality and apprearence of the workLists table, add tests * Achieve the functionality and apprearence of the workLists table, and add tests * Changeset file for PR #7640 created/updated * Changeset file for PR #7640 created/updated * Changeset file for PR #7640 created/updated * Achieve the functionality and apprearence of the work List table page, add tests, and refine based the comments * Achieve the functionality and apprearence of the work List table page, add tests, and update based the comments * Achieve the functionality and apprearence of the work List table page, add tests, and update based the comment-1 * Achieve the functionality and apprearence of the work List table page, add tests, and update based the comment-1 * Enable multiple deletion and correct the code based on comments * Enable multiple deletion and correct the code based on comments * set the advanced date format * set advanced time format and tests --------- (cherry picked from commit e70bfad24b0a1b809713ea69faf1c15ec86909cb) Signed-off-by: Qxisylolo <qianxisy@amazon.com> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: SuZhou-Joe <suzhou@amazon.com> --- changelogs/fragments/7640.yml | 2 + .../delete_workspace_modal.test.tsx.snap | 44 +- .../delete_workspace_modal.test.tsx | 64 +-- .../delete_workspace_modal.tsx | 114 ++-- .../__snapshots__/index.test.tsx.snap | 508 +++++++++++------- .../components/workspace_list/index.test.tsx | 175 ++++-- .../components/workspace_list/index.tsx | 308 ++++++++--- 7 files changed, 805 insertions(+), 410 deletions(-) create mode 100644 changelogs/fragments/7640.yml diff --git a/changelogs/fragments/7640.yml b/changelogs/fragments/7640.yml new file mode 100644 index 000000000000..5a93e3bdb5d9 --- /dev/null +++ b/changelogs/fragments/7640.yml @@ -0,0 +1,2 @@ +feat: +- [Workspace] Update workspace list page table ([#7640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7640)) \ No newline at end of file diff --git a/src/plugins/workspace/public/components/delete_workspace_modal/__snapshots__/delete_workspace_modal.test.tsx.snap b/src/plugins/workspace/public/components/delete_workspace_modal/__snapshots__/delete_workspace_modal.test.tsx.snap index c34ad92fcc2e..604d6a5edb66 100644 --- a/src/plugins/workspace/public/components/delete_workspace_modal/__snapshots__/delete_workspace_modal.test.tsx.snap +++ b/src/plugins/workspace/public/components/delete_workspace_modal/__snapshots__/delete_workspace_modal.test.tsx.snap @@ -47,49 +47,7 @@ exports[`DeleteWorkspaceModal should render normally 1`] = ` > <div class="euiModalBody__overflow" - > - <div - style="line-height: 1.5;" - > - <p> - The following workspace will be permanently deleted. This action cannot be undone. - </p> - <ul - style="list-style-type: disc; list-style-position: inside;" - /> - <div - class="euiSpacer euiSpacer--l" - /> - <div - class="euiText euiText--medium" - > - <div - class="euiTextColor euiTextColor--subdued" - > - To confirm your action, type - <b> - delete - </b> - . - </div> - </div> - <div - class="euiFormControlLayout euiFormControlLayout--fullWidth euiFormControlLayout--compressed" - > - <div - class="euiFormControlLayout__childrenWrapper" - > - <input - class="euiFieldText euiFieldText--fullWidth euiFieldText--compressed" - data-test-subj="delete-workspace-modal-input" - placeholder="delete" - type="text" - value="" - /> - </div> - </div> - </div> - </div> + /> </div> <div class="euiModalFooter" diff --git a/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.test.tsx b/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.test.tsx index 0304aa238ada..fc211cc7cbbb 100644 --- a/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.test.tsx +++ b/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.test.tsx @@ -12,7 +12,7 @@ import { OpenSearchDashboardsContextProvider } from '../../../../../plugins/open const defaultProps: DeleteWorkspaceModalProps = { onClose: jest.fn(), - selectedWorkspace: null, + selectedWorkspaces: [], onDeleteSuccess: jest.fn(), }; @@ -63,10 +63,12 @@ describe('DeleteWorkspaceModal', () => { const onDeleteSuccessFn = jest.fn(); const newProps = { ...defaultProps, - selectedWorkspace: { - id: 'test', - name: 'test', - }, + selectedWorkspaces: [ + { + id: 'test', + name: 'test', + }, + ], onClose: onCloseFn, onDeleteSuccess: onDeleteSuccessFn, }; @@ -99,10 +101,10 @@ describe('DeleteWorkspaceModal', () => { }); }); - it('should not call deleteWorkspace if passed selectedWorkspace is null', async () => { + it('should not call deleteWorkspace modal if passed selectedWorkspace is null', async () => { const newProps = { ...defaultProps, - selectedWorkspace: null, + selectedWorkspace: [], }; const deleteFn = jest.fn().mockReturnValue({ success: true, @@ -114,26 +116,20 @@ describe('DeleteWorkspaceModal', () => { delete: deleteFn, }, }; - const { getByTestId, findByTestId } = render( - getWrapWorkspaceDeleteModalInContext(newProps, newServices) - ); - await findByTestId('delete-workspace-modal-input'); - const input = getByTestId('delete-workspace-modal-input'); - fireEvent.change(input, { - target: { value: 'delete' }, - }); - const confirmButton = getByTestId('delete-workspace-modal-confirm'); - fireEvent.click(confirmButton); - expect(deleteFn).not.toHaveBeenCalled(); + const { queryByTestId } = render(getWrapWorkspaceDeleteModalInContext(newProps, newServices)); + const input = queryByTestId('delete-workspace-modal-input'); + expect(input).not.toBeInTheDocument(); }); - it('should add danger is returned data is unsuccess', async () => { + it('should add danger if returned data is unsuccess', async () => { const newProps = { ...defaultProps, - selectedWorkspace: { - id: 'test', - name: 'test', - }, + selectedWorkspaces: [ + { + id: 'test', + name: 'test', + }, + ], }; const deleteFn = jest.fn().mockReturnValue({ success: false, @@ -165,10 +161,12 @@ describe('DeleteWorkspaceModal', () => { it('confirm button should be disabled if not input delete', async () => { const newProps = { ...defaultProps, - selectedWorkspace: { - id: 'test', - name: 'test', - }, + selectedWorkspaces: [ + { + id: 'test', + name: 'test', + }, + ], }; const deleteFn = jest.fn().mockReturnValue({ success: false, @@ -186,7 +184,7 @@ describe('DeleteWorkspaceModal', () => { await findByTestId('delete-workspace-modal-input'); const input = getByTestId('delete-workspace-modal-input'); fireEvent.change(input, { - target: { value: 'delet' }, + target: { value: 'delete' }, }); const confirmButton = getByTestId('delete-workspace-modal-confirm'); expect(confirmButton.hasAttribute('disabled')); @@ -196,10 +194,12 @@ describe('DeleteWorkspaceModal', () => { const onCloseFn = jest.fn(); const newProps = { ...defaultProps, - selectedWorkspace: { - id: 'test', - name: 'test', - }, + selectedWorkspaces: [ + { + id: 'test', + name: 'test', + }, + ], onclose: onCloseFn, }; const deleteFn = jest.fn().mockImplementation(() => { diff --git a/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx b/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx index e6a3558a50d2..01a98d82bdd3 100644 --- a/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx +++ b/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx @@ -23,49 +23,53 @@ import { WorkspaceClient } from '../../workspace_client'; export interface DeleteWorkspaceModalProps { onClose: () => void; - selectedWorkspace?: WorkspaceAttribute | null; + selectedWorkspaces?: WorkspaceAttribute[]; onDeleteSuccess?: () => void; } export function DeleteWorkspaceModal(props: DeleteWorkspaceModalProps) { const [value, setValue] = useState(''); - const { onClose, selectedWorkspace, onDeleteSuccess } = props; + const { onClose, selectedWorkspaces, onDeleteSuccess } = props; const { services: { notifications, workspaceClient }, } = useOpenSearchDashboards<{ workspaceClient: WorkspaceClient }>(); - const deleteWorkspace = async () => { - if (selectedWorkspace?.id) { - let result; - try { - result = await workspaceClient.delete(selectedWorkspace?.id); - } catch (error) { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.delete.failed', { - defaultMessage: 'Failed to delete workspace', - }), - text: error instanceof Error ? error.message : JSON.stringify(error), - }); - return onClose(); - } - if (result?.success) { - notifications?.toasts.addSuccess({ - title: i18n.translate('workspace.delete.success', { - defaultMessage: 'Delete workspace successfully', - }), - }); - onClose(); - if (onDeleteSuccess) { - onDeleteSuccess(); + const deleteWorkspaces = async () => { + if (selectedWorkspaces && selectedWorkspaces.length > 0) { + selectedWorkspaces.forEach(async (selectedWorkspace) => { + if (selectedWorkspace?.id) { + let result; + try { + result = await workspaceClient.delete(selectedWorkspace?.id); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.delete.failed', { + defaultMessage: 'Failed to delete workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return onClose(); + } + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.delete.success', { + defaultMessage: 'Delete workspace successfully', + }), + }); + onClose(); + if (onDeleteSuccess) { + onDeleteSuccess(); + } + } else { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.delete.failed', { + defaultMessage: 'Failed to delete workspace', + }), + text: result?.error, + }); + } } - } else { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.delete.failed', { - defaultMessage: 'Failed to delete workspace', - }), - text: result?.error, - }); - } + }); } }; @@ -76,23 +80,31 @@ export function DeleteWorkspaceModal(props: DeleteWorkspaceModalProps) { </EuiModalHeader> <EuiModalBody data-test-subj="delete-workspace-modal-body"> - <div style={{ lineHeight: 1.5 }}> - <p>The following workspace will be permanently deleted. This action cannot be undone.</p> - <ul style={{ listStyleType: 'disc', listStylePosition: 'inside' }}> - {selectedWorkspace?.name ? <li>{selectedWorkspace.name}</li> : null} - </ul> - <EuiSpacer /> - <EuiText color="subdued"> - To confirm your action, type <b>delete</b>. - </EuiText> - <EuiCompressedFieldText - placeholder="delete" - fullWidth - value={value} - data-test-subj="delete-workspace-modal-input" - onChange={(e) => setValue(e.target.value)} - /> - </div> + {selectedWorkspaces && selectedWorkspaces.length > 0 ? ( + <div style={{ lineHeight: 1.5 }}> + <p> + The following workspace will be permanently deleted. This action cannot be undone. + </p> + <ul style={{ listStyleType: 'disc', listStylePosition: 'inside' }}> + {selectedWorkspaces.map((selectedWorkspace) => { + return selectedWorkspace?.name ? ( + <li key={selectedWorkspace.id}>{selectedWorkspace.name}</li> + ) : null; + })} + </ul> + <EuiSpacer /> + <EuiText color="subdued"> + To confirm your action, type <b>delete</b>. + </EuiText> + <EuiCompressedFieldText + placeholder="delete" + fullWidth + value={value} + data-test-subj="delete-workspace-modal-input" + onChange={(e) => setValue(e.target.value)} + /> + </div> + ) : null} </EuiModalBody> <EuiModalFooter> @@ -104,7 +116,7 @@ export function DeleteWorkspaceModal(props: DeleteWorkspaceModalProps) { </EuiSmallButtonEmpty> <EuiSmallButton data-test-subj="delete-workspace-modal-confirm" - onClick={deleteWorkspace} + onClick={deleteWorkspaces} fill color="danger" disabled={value !== 'delete'} diff --git a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap index 86989c4bbf25..c85168003d76 100644 --- a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap @@ -66,6 +66,48 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </div> </div> </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero euiSearchBar__filtersHolder" + > + <div + class="euiFilterGroup" + > + <div + class="euiPopover euiPopover--anchorDownCenter" + id="field_value_selection_0" + > + <div + class="euiPopover__anchor" + > + <button + class="euiButtonEmpty euiButtonEmpty--text euiFilterButton euiFilterButton--hasIcon" + type="button" + > + <span + class="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content" + > + <span + class="euiButtonContent__icon" + color="inherit" + data-euiicon-type="arrowDown" + /> + <span + class="euiButtonEmpty__text" + > + <span + class="euiFilterButton__textShift" + data-text="Use Case" + title="Use Case" + > + Use Case + </span> + </span> + </span> + </button> + </div> + </div> + </div> + </div> </div> <div class="euiSpacer euiSpacer--l" @@ -82,7 +124,27 @@ exports[`WorkspaceList should render title and table normally 1`] = ` > <div class="euiFlexItem euiFlexItem--flexGrowZero" - /> + > + <div + class="euiCheckbox" + > + <input + aria-label="Select all rows" + class="euiCheckbox__input" + id="_selection_column-checkbox_generated-id" + type="checkbox" + /> + <div + class="euiCheckbox__square" + /> + <label + class="euiCheckbox__label" + for="_selection_column-checkbox_generated-id" + > + Select all rows + </label> + </div> + </div> <div class="euiFlexItem euiFlexItem--flexGrowZero" > @@ -130,6 +192,29 @@ exports[`WorkspaceList should render title and table normally 1`] = ` /> <thead> <tr> + <th + class="euiTableHeaderCellCheckbox" + scope="col" + > + <div + class="euiTableCellContent" + > + <div + class="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel" + > + <input + aria-label="Select all rows" + class="euiCheckbox__input" + data-test-subj="checkboxSelectAll" + id="_selection_column-checkbox_generated-id" + type="checkbox" + /> + <div + class="euiCheckbox__square" + /> + </div> + </div> + </th> <th aria-live="polite" aria-sort="ascending" @@ -137,6 +222,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` data-test-subj="tableHeaderCell_name_0" role="columnheader" scope="col" + style="width: 25%;" > <button class="euiTableHeaderButton euiTableHeaderButton-isSorted" @@ -160,35 +246,29 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </button> </th> <th - aria-live="polite" - aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_id_1" + data-test-subj="tableHeaderCell_useCase_1" role="columnheader" scope="col" + style="width: 20%;" > - <button - class="euiTableHeaderButton" - data-test-subj="tableHeaderSortButton" - type="button" + <span + class="euiTableCellContent" > <span - class="euiTableCellContent" + class="euiTableCellContent__text" + title="Use case" > - <span - class="euiTableCellContent__text" - title="ID" - > - ID - </span> + Use case </span> - </button> + </span> </th> <th class="euiTableHeaderCell" data-test-subj="tableHeaderCell_description_2" role="columnheader" scope="col" + style="width: 20%;" > <span class="euiTableCellContent" @@ -203,18 +283,19 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </th> <th class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_features_3" + data-test-subj="tableHeaderCell_lastUpdatedTime_3" role="columnheader" scope="col" + style="width: 25%;" > <span class="euiTableCellContent" > <span class="euiTableCellContent__text" - title="Use case" + title="Last updated" > - Use case + Last updated </span> </span> </th> @@ -240,8 +321,32 @@ exports[`WorkspaceList should render title and table normally 1`] = ` <tr class="euiTableRow euiTableRow-isSelectable euiTableRow-hasActions" > + <td + class="euiTableRowCellCheckbox" + > + <div + class="euiTableCellContent" + > + <div + class="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel" + > + <input + aria-label="Select this row" + class="euiCheckbox__input" + data-test-subj="checkboxSelectRow-id1" + id="_selection_column_id1-checkbox" + title="Select this row" + type="checkbox" + /> + <div + class="euiCheckbox__square" + /> + </div> + </div> + </td> <td class="euiTableRowCell" + style="width: 25%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -258,18 +363,24 @@ exports[`WorkspaceList should render title and table normally 1`] = ` class="euiLink euiLink--primary" type="button" > - name1 + <button + class="euiLink euiLink--primary" + type="button" + > + name1 + </button> </button> </span> </div> </td> <td class="euiTableRowCell" + style="width: 20%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > - ID + Use case </div> <div class="euiTableCellContent" @@ -277,12 +388,13 @@ exports[`WorkspaceList should render title and table normally 1`] = ` <span class="euiTableCellContent__text" > - id1 + Analytics (All) </span> </div> </td> <td class="euiTableRowCell" + style="width: 20%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -290,25 +402,33 @@ exports[`WorkspaceList should render title and table normally 1`] = ` Description </div> <div - class="euiTableCellContent euiTableCellContent--truncateText" + class="euiTableCellContent euiTableCellContent--overflowingContent" > <span - class="euiTableCellContent__text" - /> + class="euiToolTipAnchor" + > + <div + class="euiText euiText--small eui-textTruncate" + style="max-width: 150px;" + > + should be able to see the description tooltip when hovering over the description + </div> + </span> </div> </td> <td - class="euiTableRowCell euiTableRowCell--hasActions euiTableRowCell--isExpander" + class="euiTableRowCell" + style="width: 25%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > - Use case + Last updated </div> <div class="euiTableCellContent euiTableCellContent--overflowingContent" > - Analytics (All) + Aug 5, 1999 @ 22:00:00.000 </div> </td> <td @@ -317,60 +437,68 @@ exports[`WorkspaceList should render title and table normally 1`] = ` <div class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--showOnHover euiTableCellContent--overflowingContent" > - <span - class="euiToolTipAnchor" - > - <button - aria-labelledby="generated-id" - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall euiTableCellContent__hoverItem" - data-test-subj="workspace-list-edit-icon" - type="button" - > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="pencil" - /> - </button> - <span - class="euiScreenReaderOnly" - id="generated-id" - > - Edit - </span> - </span> - <span - class="euiToolTipAnchor" + <div + class="euiTableCellContent__hoverItem" > - <button - aria-labelledby="generated-id" - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall euiTableCellContent__hoverItem" - data-test-subj="workspace-list-delete-icon" - type="button" + <div + class="euiPopover euiPopover--anchorLeftCenter" + id="id1-actions" > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="trash" - /> - </button> - <span - class="euiScreenReaderOnly" - id="generated-id" - > - Delete - </span> - </span> + <div + class="euiPopover__anchor" + > + <span + class="euiToolTipAnchor" + > + <button + aria-label="All actions" + class="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall" + data-test-subj="euiCollapsedItemActionsButton" + type="button" + > + <span + aria-hidden="true" + class="euiButtonIcon__icon" + color="inherit" + data-euiicon-type="boxesHorizontal" + /> + </button> + </span> + </div> + </div> + </div> </div> </td> </tr> <tr class="euiTableRow euiTableRow-isSelectable euiTableRow-hasActions" > + <td + class="euiTableRowCellCheckbox" + > + <div + class="euiTableCellContent" + > + <div + class="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel" + > + <input + aria-label="Select this row" + class="euiCheckbox__input" + data-test-subj="checkboxSelectRow-id2" + id="_selection_column_id2-checkbox" + title="Select this row" + type="checkbox" + /> + <div + class="euiCheckbox__square" + /> + </div> + </div> + </td> <td class="euiTableRowCell" + style="width: 25%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -387,18 +515,24 @@ exports[`WorkspaceList should render title and table normally 1`] = ` class="euiLink euiLink--primary" type="button" > - name2 + <button + class="euiLink euiLink--primary" + type="button" + > + name2 + </button> </button> </span> </div> </td> <td class="euiTableRowCell" + style="width: 20%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > - ID + Use case </div> <div class="euiTableCellContent" @@ -406,12 +540,13 @@ exports[`WorkspaceList should render title and table normally 1`] = ` <span class="euiTableCellContent__text" > - id2 + Observability </span> </div> </td> <td class="euiTableRowCell" + style="width: 20%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -419,24 +554,34 @@ exports[`WorkspaceList should render title and table normally 1`] = ` Description </div> <div - class="euiTableCellContent euiTableCellContent--truncateText" + class="euiTableCellContent euiTableCellContent--overflowingContent" > <span - class="euiTableCellContent__text" - /> + class="euiToolTipAnchor" + > + <div + class="euiText euiText--small eui-textTruncate" + style="max-width: 150px;" + > + should be able to see the description tooltip when hovering over the description + </div> + </span> </div> </td> <td - class="euiTableRowCell euiTableRowCell--hasActions euiTableRowCell--isExpander" + class="euiTableRowCell" + style="width: 25%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > - Use case + Last updated </div> <div class="euiTableCellContent euiTableCellContent--overflowingContent" - /> + > + Aug 5, 1999 @ 20:00:00.000 + </div> </td> <td class="euiTableRowCell euiTableRowCell--hasActions" @@ -444,60 +589,68 @@ exports[`WorkspaceList should render title and table normally 1`] = ` <div class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--showOnHover euiTableCellContent--overflowingContent" > - <span - class="euiToolTipAnchor" - > - <button - aria-labelledby="generated-id" - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall euiTableCellContent__hoverItem" - data-test-subj="workspace-list-edit-icon" - type="button" - > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="pencil" - /> - </button> - <span - class="euiScreenReaderOnly" - id="generated-id" - > - Edit - </span> - </span> - <span - class="euiToolTipAnchor" + <div + class="euiTableCellContent__hoverItem" > - <button - aria-labelledby="generated-id" - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall euiTableCellContent__hoverItem" - data-test-subj="workspace-list-delete-icon" - type="button" - > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="trash" - /> - </button> - <span - class="euiScreenReaderOnly" - id="generated-id" + <div + class="euiPopover euiPopover--anchorLeftCenter" + id="id2-actions" > - Delete - </span> - </span> + <div + class="euiPopover__anchor" + > + <span + class="euiToolTipAnchor" + > + <button + aria-label="All actions" + class="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall" + data-test-subj="euiCollapsedItemActionsButton" + type="button" + > + <span + aria-hidden="true" + class="euiButtonIcon__icon" + color="inherit" + data-euiicon-type="boxesHorizontal" + /> + </button> + </span> + </div> + </div> + </div> </div> </td> </tr> <tr class="euiTableRow euiTableRow-isSelectable euiTableRow-hasActions" > + <td + class="euiTableRowCellCheckbox" + > + <div + class="euiTableCellContent" + > + <div + class="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel" + > + <input + aria-label="Select this row" + class="euiCheckbox__input" + data-test-subj="checkboxSelectRow-id3" + id="_selection_column_id3-checkbox" + title="Select this row" + type="checkbox" + /> + <div + class="euiCheckbox__square" + /> + </div> + </div> + </td> <td class="euiTableRowCell" + style="width: 25%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -514,31 +667,36 @@ exports[`WorkspaceList should render title and table normally 1`] = ` class="euiLink euiLink--primary" type="button" > - name3 + <button + class="euiLink euiLink--primary" + type="button" + > + name3 + </button> </button> </span> </div> </td> <td class="euiTableRowCell" + style="width: 20%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > - ID + Use case </div> <div class="euiTableCellContent" > <span class="euiTableCellContent__text" - > - id3 - </span> + /> </div> </td> <td class="euiTableRowCell" + style="width: 20%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -546,25 +704,31 @@ exports[`WorkspaceList should render title and table normally 1`] = ` Description </div> <div - class="euiTableCellContent euiTableCellContent--truncateText" + class="euiTableCellContent euiTableCellContent--overflowingContent" > <span - class="euiTableCellContent__text" - /> + class="euiToolTipAnchor" + > + <div + class="euiText euiText--small eui-textTruncate" + style="max-width: 150px;" + /> + </span> </div> </td> <td - class="euiTableRowCell euiTableRowCell--hasActions euiTableRowCell--isExpander" + class="euiTableRowCell" + style="width: 25%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" > - Use case + Last updated </div> <div class="euiTableCellContent euiTableCellContent--overflowingContent" > - Observability + Aug 5, 1999 @ 21:00:00.000 </div> </td> <td @@ -573,52 +737,36 @@ exports[`WorkspaceList should render title and table normally 1`] = ` <div class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--showOnHover euiTableCellContent--overflowingContent" > - <span - class="euiToolTipAnchor" - > - <button - aria-labelledby="generated-id" - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall euiTableCellContent__hoverItem" - data-test-subj="workspace-list-edit-icon" - type="button" - > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="pencil" - /> - </button> - <span - class="euiScreenReaderOnly" - id="generated-id" - > - Edit - </span> - </span> - <span - class="euiToolTipAnchor" + <div + class="euiTableCellContent__hoverItem" > - <button - aria-labelledby="generated-id" - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall euiTableCellContent__hoverItem" - data-test-subj="workspace-list-delete-icon" - type="button" - > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="trash" - /> - </button> - <span - class="euiScreenReaderOnly" - id="generated-id" + <div + class="euiPopover euiPopover--anchorLeftCenter" + id="id3-actions" > - Delete - </span> - </span> + <div + class="euiPopover__anchor" + > + <span + class="euiToolTipAnchor" + > + <button + aria-label="All actions" + class="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall" + data-test-subj="euiCollapsedItemActionsButton" + type="button" + > + <span + aria-hidden="true" + class="euiButtonIcon__icon" + color="inherit" + data-euiicon-type="boxesHorizontal" + /> + </button> + </span> + </div> + </div> + </div> </div> </td> </tr> diff --git a/src/plugins/workspace/public/components/workspace_list/index.test.tsx b/src/plugins/workspace/public/components/workspace_list/index.test.tsx index 2284008d9d36..f45f5feaf860 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.test.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.test.tsx @@ -4,6 +4,7 @@ */ import React from 'react'; +import moment from 'moment'; import { BehaviorSubject, of } from 'rxjs'; import { render, fireEvent, screen } from '@testing-library/react'; import { I18nProvider } from '@osd/i18n/react'; @@ -15,6 +16,18 @@ import { WorkspaceList } from './index'; jest.mock('../utils/workspace'); +const mockNavigatorWrite = jest.fn(); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + copyToClipboard: jest.fn().mockImplementation((id) => { + mockNavigatorWrite(id); + }), + }; +}); + jest.mock('../delete_workspace_modal', () => ({ DeleteWorkspaceModal: ({ onClose }: { onClose: () => void }) => ( <div aria-label="mock delete workspace modal"> @@ -25,9 +38,29 @@ jest.mock('../delete_workspace_modal', () => ({ function getWrapWorkspaceListInContext( workspaceList = [ - { id: 'id1', name: 'name1', features: ['use-case-all'] }, - { id: 'id2', name: 'name2' }, - { id: 'id3', name: 'name3', features: ['use-case-observability'] }, + { + id: 'id1', + name: 'name1', + features: ['use-case-all'], + description: + 'should be able to see the description tooltip when hovering over the description', + lastUpdatedTime: '1999-08-06T02:00:00.00Z', + }, + { + id: 'id2', + name: 'name2', + features: ['use-case-observability'], + description: + 'should be able to see the description tooltip when hovering over the description', + lastUpdatedTime: '1999-08-06T00:00:00.00Z', + }, + { + id: 'id3', + name: 'name3', + features: ['use-case-search'], + description: '', + lastUpdatedTime: '1999-08-06T01:00:00.00Z', + }, ], isDashboardAdmin = true ) { @@ -48,6 +81,14 @@ function getWrapWorkspaceListInContext( workspaces: { workspaceList$: of(workspaceList), }, + uiSettings: { + get: jest.fn().mockImplementation((key) => { + if (key === 'dateFormat') { + return 'MMM D, YYYY @ HH:mm:ss.SSS'; + } + return null; + }), + }, navigationUI: { HeaderControl: mockHeaderControl, }, @@ -84,26 +125,27 @@ describe('WorkspaceList', () => { expect(getByText('Analytics (All)')).toBeInTheDocument(); expect(getByText('Observability')).toBeInTheDocument(); }); - it('should be able to apply debounce search after input', async () => { - const list = [ - { id: 'id1', name: 'name1' }, - { id: 'id2', name: 'name2' }, - { id: 'id3', name: 'name3' }, - { id: 'id4', name: 'name4' }, - { id: 'id5', name: 'name5' }, - { id: 'id6', name: 'name6' }, - ]; - const { getByText, getByRole, queryByText } = render(getWrapWorkspaceListInContext(list)); - expect(getByText('name1')).toBeInTheDocument(); - expect(queryByText('name6')).not.toBeInTheDocument(); + + it('should be able to search and re-render the list', async () => { + const { getByText, getByRole, queryByText } = render(getWrapWorkspaceListInContext()); const input = getByRole('searchbox'); fireEvent.change(input, { - target: { value: 'nam' }, + target: { value: 'name2' }, }); + expect(getByText('name2')).toBeInTheDocument(); + expect(queryByText('name1')).not.toBeInTheDocument(); + expect(queryByText('name3')).not.toBeInTheDocument(); + }); + + it('should be able to apply debounce search after input', async () => { + const { getByText, getByRole, queryByText } = render(getWrapWorkspaceListInContext()); + const input = getByRole('searchbox'); fireEvent.change(input, { - target: { value: 'name6' }, + target: { value: 'name2' }, }); - expect(queryByText('name6')).not.toBeInTheDocument(); + expect(getByText('name2')).toBeInTheDocument(); + expect(queryByText('name1')).not.toBeInTheDocument(); + expect(queryByText('name3')).not.toBeInTheDocument(); }); it('should be able to switch workspace after clicking name', async () => { @@ -113,16 +155,51 @@ describe('WorkspaceList', () => { expect(navigateToWorkspaceDetail).toBeCalled(); }); + it('should be able to perform the time format transformation', async () => { + const { getByText } = render(getWrapWorkspaceListInContext()); + expect( + getByText(moment('1999-08-06T00:00:00.00Z').format('MMM D, YYYY @ HH:mm:ss.SSS')) + ).toBeInTheDocument(); + expect( + getByText(moment('1999-08-06T01:00:00.00Z').format('MMM D, YYYY @ HH:mm:ss.SSS')) + ).toBeInTheDocument(); + expect( + getByText(moment('1999-08-06T02:00:00.00Z').format('MMM D, YYYY @ HH:mm:ss.SSS')) + ).toBeInTheDocument(); + }); + + it('should be able to see the 3 operations: copy, update, delete after click in the meatballs button', async () => { + const { getAllByTestId, getByText } = render(getWrapWorkspaceListInContext()); + const operationIcons = getAllByTestId('euiCollapsedItemActionsButton')[0]; + fireEvent.click(operationIcons); + expect(getByText('Copy ID')).toBeInTheDocument(); + expect(getByText('Edit')).toBeInTheDocument(); + expect(getByText('Delete')).toBeInTheDocument(); + }); + + it('should be able to copy workspace ID after clicking copy button', async () => { + const { getByText, getAllByTestId } = render(getWrapWorkspaceListInContext()); + const operationIcons = getAllByTestId('euiCollapsedItemActionsButton')[0]; + fireEvent.click(operationIcons); + const copyIcon = getByText('Copy ID'); + fireEvent.click(copyIcon); + expect(mockNavigatorWrite).toHaveBeenCalledWith('id1'); + }); + it('should be able to update workspace after clicking name', async () => { - const { getAllByTestId } = render(getWrapWorkspaceListInContext()); - const editIcon = getAllByTestId('workspace-list-edit-icon')[0]; + const { getByText, getAllByTestId } = render(getWrapWorkspaceListInContext()); + const operationIcons = getAllByTestId('euiCollapsedItemActionsButton')[0]; + fireEvent.click(operationIcons); + const editIcon = getByText('Edit'); fireEvent.click(editIcon); expect(navigateToWorkspaceDetail).toBeCalled(); }); it('should be able to call delete modal after clicking delete button', async () => { - const { getAllByTestId } = render(getWrapWorkspaceListInContext()); - const deleteIcon = getAllByTestId('workspace-list-delete-icon')[0]; + const { getByText, getAllByTestId } = render(getWrapWorkspaceListInContext()); + const operationIcons = getAllByTestId('euiCollapsedItemActionsButton')[0]; + fireEvent.click(operationIcons); + const deleteIcon = getByText('Delete'); fireEvent.click(deleteIcon); expect(screen.queryByLabelText('mock delete workspace modal')).toBeInTheDocument(); const modalCancelButton = screen.getByLabelText('mock delete workspace modal button'); @@ -132,12 +209,48 @@ describe('WorkspaceList', () => { it('should be able to pagination when clicking pagination button', async () => { const list = [ - { id: 'id1', name: 'name1' }, - { id: 'id2', name: 'name2' }, - { id: 'id3', name: 'name3' }, - { id: 'id4', name: 'name4' }, - { id: 'id5', name: 'name5' }, - { id: 'id6', name: 'name6' }, + { + id: 'id1', + name: 'name1', + features: ['use-case-all'], + description: '', + lastUpdatedTime: '2024-08-06T00:00:00.00Z', + }, + { + id: 'id2', + name: 'name2', + features: ['use-case-observability'], + description: '', + lastUpdatedTime: '2024-08-06T00:00:00.00Z', + }, + { + id: 'id3', + name: 'name3', + features: ['use-case-search'], + description: '', + lastUpdatedTime: '2024-08-06T00:00:00.00Z', + }, + { + id: 'id4', + name: 'name4', + features: ['use-case-all'], + description: '', + lastUpdatedTime: '2024-08-05T00:00:00.00Z', + }, + { + id: 'id5', + name: 'name5', + features: ['use-case-observability'], + description: '', + lastUpdatedTime: '2024-08-06T00:00:00.00Z', + }, + { + id: 'id6', + name: 'name6', + features: ['use-case-search'], + description: '', + lastUpdatedTime: '2024-08-06T00:00:00.00Z', + }, ]; const { getByTestId, getByText, queryByText } = render(getWrapWorkspaceListInContext(list)); expect(getByText('name1')).toBeInTheDocument(); @@ -149,14 +262,12 @@ describe('WorkspaceList', () => { }); it('should display create workspace button for dashboard admin', async () => { - const { getByText } = render(getWrapWorkspaceListInContext([], true)); - - expect(getByText('Create workspace')).toBeInTheDocument(); + const { getAllByText } = render(getWrapWorkspaceListInContext([], true)); + expect(getAllByText('Create workspace')[0]).toBeInTheDocument(); }); it('should hide create workspace button for non dashboard admin', async () => { const { queryByText } = render(getWrapWorkspaceListInContext([], false)); - expect(queryByText('Create workspace')).toBeNull(); }); }); diff --git a/src/plugins/workspace/public/components/workspace_list/index.tsx b/src/plugins/workspace/public/components/workspace_list/index.tsx index 931862b919a7..91fa716890bc 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.tsx @@ -4,19 +4,26 @@ */ import React, { useState, useMemo, useCallback } from 'react'; +import moment from 'moment'; import { EuiPage, EuiPageContent, EuiLink, EuiSmallButton, EuiInMemoryTable, + EuiToolTip, + EuiText, EuiSearchBarProps, + copyToClipboard, + EuiTableSelectionType, + EuiButtonEmpty, + EuiButton, + EuiEmptyPrompt, } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; import { BehaviorSubject, of } from 'rxjs'; import { i18n } from '@osd/i18n'; -import { debounce, DEFAULT_NAV_GROUPS } from '../../../../../core/public'; -import { WorkspaceAttribute } from '../../../../../core/public'; +import { DEFAULT_NAV_GROUPS, WorkspaceAttribute } from '../../../../../core/public'; import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; import { navigateToWorkspaceDetail } from '../utils/workspace'; @@ -31,6 +38,10 @@ export interface WorkspaceListProps { registeredUseCases$: BehaviorSubject<WorkspaceUseCase[]>; } +interface WorkspaceAttributeWithUseCaseID extends WorkspaceAttribute { + useCase?: string; +} + export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { const { services: { @@ -38,47 +49,52 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { application, http, navigationUI: { HeaderControl }, + uiSettings, }, } = useOpenSearchDashboards<{ navigationUI: NavigationPublicPluginStart['ui']; }>(); const registeredUseCases = useObservable(registeredUseCases$); const isDashboardAdmin = application?.capabilities?.dashboards?.isDashboardAdmin; - const initialSortField = 'name'; const initialSortDirection = 'asc'; const workspaceList = useObservable(workspaces?.workspaceList$ ?? of([]), []); - const [queryInput, setQueryInput] = useState<string>(''); + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5, pageSizeOptions: [5, 10, 20], }); - const [deletedWorkspace, setDeletedWorkspace] = useState<WorkspaceAttribute | null>(null); + const [deletedWorkspaces, setDeletedWorkspaces] = useState<WorkspaceAttribute[]>([]); + const [selection, setSelection] = useState<WorkspaceAttribute[]>([]); - const handleSwitchWorkspace = useCallback( - (id: string) => { - if (application && http) { - navigateToWorkspaceDetail({ application, http }, id); + const dateFormat = uiSettings?.get('dateFormat'); + + const extractUseCaseFromFeatures = useCallback( + (features: string[]) => { + if (!features || features.length === 0) { + return ''; + } + const useCaseId = getFirstUseCaseOfFeatureConfigs(features); + const usecase = + useCaseId === DEFAULT_NAV_GROUPS.all.id + ? DEFAULT_NAV_GROUPS.all + : registeredUseCases?.find(({ id }) => id === useCaseId); + if (usecase) { + return usecase.title; } }, - [application, http] + [registeredUseCases] ); - const searchResult = useMemo(() => { - if (queryInput) { - const normalizedQuery = queryInput.toLowerCase(); - const result = workspaceList.filter((item) => { - return ( - item.id.toLowerCase().indexOf(normalizedQuery) > -1 || - item.name.toLowerCase().indexOf(normalizedQuery) > -1 - ); - }); - return result; - } - return workspaceList; - }, [workspaceList, queryInput]); - + const newWorkspaceList: WorkspaceAttributeWithUseCaseID[] = useMemo(() => { + return workspaceList.map( + (workspace): WorkspaceAttributeWithUseCaseID => ({ + ...workspace, + useCase: extractUseCaseFromFeatures(workspace.features ?? []), + }) + ); + }, [workspaceList, extractUseCaseFromFeatures]); const workspaceCreateUrl = useMemo(() => { if (!application) { return ''; @@ -92,6 +108,38 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { return appUrl; }, [application]); + const emptyStateMessage = useMemo(() => { + return ( + <EuiEmptyPrompt + iconType="spacesApp" + title={ + <h3> + {i18n.translate('workspace.workspaceList.emptyState.title', { + defaultMessage: 'No workspace available', + })} + </h3> + } + titleSize="s" + body={i18n.translate('workspace.workspaceList.emptyState.body', { + defaultMessage: 'There are no workspace to display. Create workspace to get started.', + })} + actions={ + isDashboardAdmin && ( + <EuiSmallButton + href={workspaceCreateUrl} + key="create_workspace" + data-test-subj="workspaceList-create-workspace" + > + {i18n.translate('workspace.workspaceList.buttons.createWorkspace', { + defaultMessage: 'Create workspace', + })} + </EuiSmallButton> + ) + } + /> + ); + }, [isDashboardAdmin, workspaceCreateUrl]); + const renderCreateWorkspaceButton = () => { const button = ( <EuiSmallButton @@ -113,88 +161,201 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { ); }; + const handleCopyId = (id: string) => { + copyToClipboard(id); + }; + + const handleSwitchWorkspace = useCallback( + (id: string) => { + if (application && http) { + navigateToWorkspaceDetail({ application, http }, id); + } + }, + [application, http] + ); + + const renderToolsLeft = () => { + if (selection.length === 0) { + return; + } + + const onClick = () => { + const deleteWorkspacesByIds = (workSpaces: WorkspaceAttribute[], ids: string[]) => { + const needToBeDeletedWorkspaceList: WorkspaceAttribute[] = []; + ids.forEach((id) => { + const index = workSpaces.findIndex((workSpace) => workSpace.id === id); + if (index >= 0) { + needToBeDeletedWorkspaceList.push(workSpaces[index]); + } + }); + return needToBeDeletedWorkspaceList; + }; + + setDeletedWorkspaces( + deleteWorkspacesByIds( + newWorkspaceList, + selection.map((item) => item.id) + ) + ); + + setSelection([]); + }; + + return ( + <> + <EuiButton color="danger" iconType="trash" onClick={onClick}> + Delete {selection.length} Workspace + </EuiButton> + {deletedWorkspaces && deletedWorkspaces.length > 0 && ( + <DeleteWorkspaceModal + selectedWorkspaces={deletedWorkspaces} + onClose={() => setDeletedWorkspaces([])} + /> + )} + </> + ); + }; + + const selectionValue: EuiTableSelectionType<WorkspaceAttribute> = { + onSelectionChange: (deletedSelection) => setSelection(deletedSelection), + }; + + const search: EuiSearchBarProps = { + box: { + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'useCase', + name: 'Use Case', + multiSelect: false, + options: Array.from( + new Set(newWorkspaceList.map(({ useCase }) => useCase).filter(Boolean)) + ).map((useCase) => ({ + value: useCase!, + name: useCase!, + })), + }, + ], + toolsLeft: renderToolsLeft(), + }; + const columns = [ { field: 'name', name: 'Name', + width: '25%', sortable: true, render: (name: string, item: WorkspaceAttribute) => ( <span> - <EuiLink onClick={() => handleSwitchWorkspace(item.id)}>{name}</EuiLink> + <EuiLink onClick={() => handleSwitchWorkspace(item.id)}> + <EuiLink>{name}</EuiLink> + </EuiLink> </span> ), }, + { - field: 'id', - name: 'ID', - sortable: true, + field: 'useCase', + name: 'Use case', + width: '20%', }, + { field: 'description', name: 'Description', - truncateText: true, + width: '20%', + render: (description: string) => ( + <EuiToolTip + position="bottom" + content={description} + data-test-subj="workspaceList-hover-description" + > + {/* Here I need to set width mannuly as the tooltip will ineffect the property : truncateText ', */} + <EuiText size="s" className="eui-textTruncate" style={{ maxWidth: 150 }}> + {description} + </EuiText> + </EuiToolTip> + ), }, { - field: 'features', - name: 'Use case', - isExpander: true, - hasActions: true, - render: (features: string[]) => { - if (!features || features.length === 0) { - return ''; - } - const useCaseId = getFirstUseCaseOfFeatureConfigs(features); - const useCase = - useCaseId === DEFAULT_NAV_GROUPS.all.id - ? DEFAULT_NAV_GROUPS.all - : registeredUseCases?.find(({ id }) => id === useCaseId); - if (useCase) { - return useCase.title; - } + field: 'lastUpdatedTime', + name: 'Last updated', + width: '25%', + truncateText: false, + render: (lastUpdatedTime: string) => { + return moment(lastUpdatedTime).format(dateFormat); }, }, + { name: 'Actions', field: '', actions: [ + { + name: 'Copy ID', + type: 'button', + description: 'Copy id', + 'data-test-subj': 'workspace-list-copy-id-icon', + render: ({ id }: WorkspaceAttribute) => { + return ( + <EuiButtonEmpty + onClick={() => handleCopyId(id)} + size="xs" + iconType="copy" + color="text" + > + <EuiText size="m">Copy ID</EuiText> + </EuiButtonEmpty> + ); + }, + }, { name: 'Edit', - icon: 'pencil', type: 'icon', + icon: 'edit', + color: 'danger', description: 'Edit workspace', - onClick: ({ id }: WorkspaceAttribute) => handleSwitchWorkspace(id), 'data-test-subj': 'workspace-list-edit-icon', + onClick: ({ id }: WorkspaceAttribute) => handleSwitchWorkspace(id), + render: ({ id }: WorkspaceAttribute) => { + return ( + <EuiButtonEmpty + onClick={() => handleSwitchWorkspace(id)} + iconType="pencil" + size="xs" + color="text" + > + <EuiText size="m">Edit</EuiText> + </EuiButtonEmpty> + ); + }, }, { name: 'Delete', - icon: 'trash', - type: 'icon', + type: 'button', description: 'Delete workspace', - onClick: (item: WorkspaceAttribute) => setDeletedWorkspace(item), 'data-test-subj': 'workspace-list-delete-icon', + render: (item: WorkspaceAttribute) => { + return ( + <EuiButtonEmpty + onClick={() => { + setDeletedWorkspaces([item]); + }} + size="xs" + iconType="trash" + color="danger" + > + <EuiText size="m">Delete</EuiText> + </EuiButtonEmpty> + ); + }, }, ], }, ]; - const debouncedSetQueryInput = useMemo(() => { - return debounce(setQueryInput, 300); - }, [setQueryInput]); - - const handleSearchInput: EuiSearchBarProps['onChange'] = useCallback( - ({ query }) => { - debouncedSetQueryInput(query?.text ?? ''); - }, - [debouncedSetQueryInput] - ); - - const search: EuiSearchBarProps = { - onChange: handleSearchInput, - box: { - incremental: true, - }, - }; - return ( <EuiPage paddingSize="m"> <HeaderControl @@ -216,9 +377,10 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { hasShadow={false} > <EuiInMemoryTable - items={searchResult} + items={newWorkspaceList} columns={columns} itemId="id" + message={emptyStateMessage} onTableChange={({ page: { index, size } }) => setPagination((prev) => { return { ...prev, pageIndex: index, pageSize: size }; @@ -233,12 +395,14 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { }} isSelectable={true} search={search} + selection={selectionValue} /> </EuiPageContent> - {deletedWorkspace && ( + + {deletedWorkspaces.length > 0 && ( <DeleteWorkspaceModal - selectedWorkspace={deletedWorkspace} - onClose={() => setDeletedWorkspace(null)} + selectedWorkspaces={deletedWorkspaces} + onClose={() => setDeletedWorkspaces([])} /> )} </EuiPage>