Skip to content

Commit

Permalink
[Discover] Add "Shift + Select" functionality to Discover grid (elast…
Browse files Browse the repository at this point in the history
…ic#193619)

- Closes elastic#192366

## Summary

This PR allows to select/deselect multiple rows by holding SHIFT key
when toggling row checkboxes.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

(cherry picked from commit 6808f82)
  • Loading branch information
jughosta committed Sep 25, 2024
1 parent 41bb84d commit b4c1a66
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 10 deletions.
1 change: 1 addition & 0 deletions packages/kbn-unified-data-table/__mocks__/table_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function buildSelectedDocsState(selectedDocIds: string[]): UseSelectedDoc
selectedDocsCount: selectedDocsSet.size,
docIdsInSelectionOrder: selectedDocIds,
toggleDocSelection: jest.fn(),
toggleMultipleDocsSelection: jest.fn(),
selectAllDocs: jest.fn(),
selectMoreDocs: jest.fn(),
deselectSomeDocs: jest.fn(),
Expand Down
10 changes: 8 additions & 2 deletions packages/kbn-unified-data-table/src/components/data_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -497,8 +497,14 @@ export const UnifiedDataTable = ({
const [isCompareActive, setIsCompareActive] = useState(false);
const displayedColumns = getDisplayedColumns(columns, dataView);
const defaultColumns = displayedColumns.includes('_source');
const docMap = useMemo(() => new Map(rows?.map((row) => [row.id, row]) ?? []), [rows]);
const getDocById = useCallback((id: string) => docMap.get(id), [docMap]);
const docMap = useMemo(
() =>
new Map<string, { doc: DataTableRecord; docIndex: number }>(
rows?.map((row, docIndex) => [row.id, { doc: row, docIndex }]) ?? []
),
[rows]
);
const getDocById = useCallback((id: string) => docMap.get(id)?.doc, [docMap]);
const selectedDocsState = useSelectedDocs(docMap);
const {
isDocSelected,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const SelectButton = (props: EuiDataGridCellValueElementProps) => {
const { record, rowIndex } = useControlColumn(props);
const { euiTheme } = useEuiTheme();
const { selectedDocsState } = useContext(UnifiedDataTableContext);
const { isDocSelected, toggleDocSelection } = selectedDocsState;
const { isDocSelected, toggleDocSelection, toggleMultipleDocsSelection } = selectedDocsState;

const toggleDocumentSelectionLabel = i18n.translate('unifiedDataTable.grid.selectDoc', {
defaultMessage: `Select document ''{rowNumber}''`,
Expand Down Expand Up @@ -66,8 +66,12 @@ export const SelectButton = (props: EuiDataGridCellValueElementProps) => {
aria-label={toggleDocumentSelectionLabel}
checked={isDocSelected(record.id)}
data-test-subj={`dscGridSelectDoc-${record.id}`}
onChange={() => {
toggleDocSelection(record.id);
onChange={(event) => {
if ((event.nativeEvent as MouseEvent)?.shiftKey) {
toggleMultipleDocsSelection(record.id);
} else {
toggleDocSelection(record.id);
}
}}
/>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('useSelectedDocs', () => {
const docs = generateEsHits(dataViewWithTimefieldMock, 5).map((hit) =>
buildDataTableRecord(hit, dataViewWithTimefieldMock)
);
const docsMap = new Map(docs.map((doc) => [doc.id, doc]));
const docsMap = new Map(docs.map((doc, docIndex) => [doc.id, { doc, docIndex }]));

test('should have a correct default state', () => {
const { result } = renderHook(() => useSelectedDocs(docsMap));
Expand Down Expand Up @@ -223,4 +223,30 @@ describe('useSelectedDocs', () => {
expect(result.current.getCountOfFilteredSelectedDocs([docs[0].id])).toBe(0);
expect(result.current.getCountOfFilteredSelectedDocs([docs[2].id, docs[3].id])).toBe(0);
});

test('should toggleMultipleDocsSelection correctly', () => {
const { result } = renderHook(() => useSelectedDocs(docsMap));
const docIds = docs.map((doc) => doc.id);

// select `0`
act(() => {
result.current.toggleDocSelection(docs[0].id);
});

expect(result.current.getCountOfFilteredSelectedDocs(docIds)).toBe(1);

// select from `0` to `4`
act(() => {
result.current.toggleMultipleDocsSelection(docs[4].id);
});

expect(result.current.getCountOfFilteredSelectedDocs(docIds)).toBe(5);

// deselect from `2` to `4`
act(() => {
result.current.toggleMultipleDocsSelection(docs[2].id);
});

expect(result.current.getCountOfFilteredSelectedDocs(docIds)).toBe(2);
});
});
47 changes: 45 additions & 2 deletions packages/kbn-unified-data-table/src/hooks/use_selected_docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import type { DataTableRecord } from '@kbn/discover-utils';

export interface UseSelectedDocsState {
Expand All @@ -17,6 +17,7 @@ export interface UseSelectedDocsState {
selectedDocsCount: number;
docIdsInSelectionOrder: string[];
toggleDocSelection: (docId: string) => void;
toggleMultipleDocsSelection: (toDocId: string) => void;
selectAllDocs: () => void;
selectMoreDocs: (docIds: string[]) => void;
deselectSomeDocs: (docIds: string[]) => void;
Expand All @@ -25,8 +26,11 @@ export interface UseSelectedDocsState {
getSelectedDocsOrderedByRows: (rows: DataTableRecord[]) => DataTableRecord[];
}

export const useSelectedDocs = (docMap: Map<string, DataTableRecord>): UseSelectedDocsState => {
export const useSelectedDocs = (
docMap: Map<string, { doc: DataTableRecord; docIndex: number }>
): UseSelectedDocsState => {
const [selectedDocsSet, setSelectedDocsSet] = useState<Set<string>>(new Set());
const lastCheckboxToggledDocId = useRef<string | undefined>();

const toggleDocSelection = useCallback((docId: string) => {
setSelectedDocsSet((prevSelectedRowsSet) => {
Expand All @@ -38,6 +42,7 @@ export const useSelectedDocs = (docMap: Map<string, DataTableRecord>): UseSelect
}
return newSelectedRowsSet;
});
lastCheckboxToggledDocId.current = docId;
}, []);

const replaceSelectedDocs = useCallback((docIds: string[]) => {
Expand Down Expand Up @@ -73,6 +78,42 @@ export const useSelectedDocs = (docMap: Map<string, DataTableRecord>): UseSelect
[selectedDocsSet, docMap]
);

const toggleMultipleDocsSelection = useCallback(
(toDocId: string) => {
const shouldSelect = !isDocSelected(toDocId);

const lastToggledDocIdIndex = docMap.get(
lastCheckboxToggledDocId.current ?? toDocId
)?.docIndex;
const currentToggledDocIdIndex = docMap.get(toDocId)?.docIndex;
const docIds: string[] = [];

if (
typeof lastToggledDocIdIndex === 'number' &&
typeof currentToggledDocIdIndex === 'number' &&
lastToggledDocIdIndex !== currentToggledDocIdIndex
) {
const startIndex = Math.min(lastToggledDocIdIndex, currentToggledDocIdIndex);
const endIndex = Math.max(lastToggledDocIdIndex, currentToggledDocIdIndex);

docMap.forEach(({ doc, docIndex }) => {
if (docIndex >= startIndex && docIndex <= endIndex) {
docIds.push(doc.id);
}
});
}

if (shouldSelect) {
selectMoreDocs(docIds);
} else {
deselectSomeDocs(docIds);
}

lastCheckboxToggledDocId.current = toDocId;
},
[selectMoreDocs, deselectSomeDocs, docMap, isDocSelected]
);

const getSelectedDocsOrderedByRows = useCallback(
(rows: DataTableRecord[]) => {
return rows.filter((row) => isDocSelected(row.id));
Expand Down Expand Up @@ -101,6 +142,7 @@ export const useSelectedDocs = (docMap: Map<string, DataTableRecord>): UseSelect
docIdsInSelectionOrder: selectedDocIds,
getCountOfFilteredSelectedDocs,
toggleDocSelection,
toggleMultipleDocsSelection,
selectAllDocs,
selectMoreDocs,
deselectSomeDocs,
Expand All @@ -112,6 +154,7 @@ export const useSelectedDocs = (docMap: Map<string, DataTableRecord>): UseSelect
isDocSelected,
getCountOfFilteredSelectedDocs,
toggleDocSelection,
toggleMultipleDocsSelection,
selectAllDocs,
selectMoreDocs,
deselectSomeDocs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,60 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});

it('should be able to select multiple rows holding Shift key', async () => {
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(false);

// select 1 row
await dataGrid.selectRow(1);

await retry.try(async () => {
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(true);
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(1);
expect(await dataGrid.getNumberOfSelectedRows()).to.be(1);
});

// select 3 more
await dataGrid.selectRow(4, { pressShiftKey: true });

await retry.try(async () => {
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(true);
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(4);
expect(await dataGrid.getNumberOfSelectedRows()).to.be(4);
});

// deselect index 3 and 4
await dataGrid.selectRow(3, { pressShiftKey: true });

await retry.try(async () => {
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(true);
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(2);
expect(await dataGrid.getNumberOfSelectedRows()).to.be(2);
});

// select from index 3 to 0
await dataGrid.selectRow(0, { pressShiftKey: true });

await retry.try(async () => {
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(true);
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(4);
expect(await dataGrid.getNumberOfSelectedRows()).to.be(4);
});

// select from both pages
await testSubjects.click('pagination-button-1');
await retry.try(async () => {
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(0);
});

await dataGrid.selectRow(2, { pressShiftKey: true });

await retry.try(async () => {
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(true);
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(3);
expect(await dataGrid.getNumberOfSelectedRows()).to.be(8);
});
});

it('should be able to bulk select rows', async () => {
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(false);
expect(await testSubjects.getAttribute('selectAllDocsOnPageToggle', 'title')).to.be(
Expand Down
13 changes: 11 additions & 2 deletions test/functional/services/data_grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,12 +689,21 @@ export class DataGridService extends FtrService {
await this.checkCurrentRowsPerPageToBe(newValue);
}

public async selectRow(rowIndex: number) {
public async selectRow(rowIndex: number, { pressShiftKey }: { pressShiftKey?: boolean } = {}) {
const checkbox = await this.find.byCssSelector(
`.euiDataGridRow[data-grid-visible-row-index="${rowIndex}"] [data-gridcell-column-id="select"] .euiCheckbox__input`
);

await checkbox.click();
if (pressShiftKey) {
await this.browser
.getActions()
.keyDown(Key.SHIFT)
.click(checkbox._webElement)
.keyUp(Key.SHIFT)
.perform();
} else {
await checkbox.click();
}
}

public async getNumberOfSelectedRows() {
Expand Down

0 comments on commit b4c1a66

Please sign in to comment.