mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Discover] Allow to select/deselect all rows in the grid at once (#184241)
- Closes https://github.com/elastic/kibana/issues/175943 ## Summary This PR adds a checkbox which allows now to select all rows at once (or deselect all) on the current page. - [x] A new checkbox was added to the grid header - [x] "Compare documents" button was moved under "Selected" menu - [x] "Compare documents" button gets disabled if user selects more than 100 rows - [x] "Selected" menu button got a new look - [x] A new "Select all X" button was added next too "Selected" menu button <img width="1554" alt="Screenshot 2024-07-18 at 14 45 00" src="https://github.com/user-attachments/assets/631cd350-be7d-43be-bc07-c0f6a943bacb"> <img width="563" alt="Screenshot 2024-07-18 at 14 45 10" src="https://github.com/user-attachments/assets/d49d18f2-d255-401f-b157-3892e6f78d7c"> <img width="443" alt="Screenshot 2024-07-18 at 14 47 02" src="https://github.com/user-attachments/assets/154c8292-c9b3-409d-b9f6-f78ac83527e9"> ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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 renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Davis McPhee <davismcphee@hotmail.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c0b60fef4c
commit
22de72d022
28 changed files with 1276 additions and 314 deletions
|
@ -15,6 +15,7 @@ import { DataTableContext } from '../src/table_context';
|
|||
import { convertValueToString } from '../src/utils/convert_value_to_string';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import type { EsHitRecord } from '@kbn/discover-utils/types';
|
||||
import type { UseSelectedDocsState } from '../src/hooks/use_selected_docs';
|
||||
|
||||
const buildTableContext = (dataView: DataView, rows: EsHitRecord[]): DataTableContext => {
|
||||
const usedRows = rows.map((row) => {
|
||||
|
@ -28,8 +29,9 @@ const buildTableContext = (dataView: DataView, rows: EsHitRecord[]): DataTableCo
|
|||
onFilter: jest.fn(),
|
||||
dataView,
|
||||
isDarkMode: false,
|
||||
selectedDocs: [],
|
||||
setSelectedDocs: jest.fn(),
|
||||
selectedDocsState: buildSelectedDocsState([]),
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
valueToStringConverter: (rowIndex, columnId, options) =>
|
||||
convertValueToString({
|
||||
rowIndex,
|
||||
|
@ -45,3 +47,21 @@ const buildTableContext = (dataView: DataView, rows: EsHitRecord[]): DataTableCo
|
|||
export const dataTableContextMock = buildTableContext(dataViewMock, esHitsMock);
|
||||
|
||||
export const dataTableContextComplexMock = buildTableContext(dataViewComplexMock, esHitsComplex);
|
||||
|
||||
export function buildSelectedDocsState(selectedDocIds: string[]): UseSelectedDocsState {
|
||||
const selectedDocsSet = new Set(selectedDocIds);
|
||||
|
||||
return {
|
||||
isDocSelected: (docId: string) => selectedDocsSet.has(docId),
|
||||
getCountOfSelectedDocs: (docIds: string[]) =>
|
||||
docIds.reduce((acc, docId) => (selectedDocsSet.has(docId) ? acc + 1 : acc), 0),
|
||||
hasSelectedDocs: selectedDocsSet.size > 0,
|
||||
selectedDocIds,
|
||||
toggleDocSelection: jest.fn(),
|
||||
selectAllDocs: jest.fn(),
|
||||
selectMoreDocs: jest.fn(),
|
||||
deselectSomeDocs: jest.fn(),
|
||||
replaceSelectedDocs: jest.fn(),
|
||||
clearAllSelectedDocs: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ const getDocById = (id: string) => docs.find((doc) => doc.raw._id === id);
|
|||
const renderCompareDocuments = ({
|
||||
forceShowAllFields = false,
|
||||
}: { forceShowAllFields?: boolean } = {}) => {
|
||||
const setSelectedDocs = jest.fn();
|
||||
const replaceSelectedDocs = jest.fn();
|
||||
const getCompareDocuments = (props?: Partial<CompareDocumentsProps>) => (
|
||||
<CompareDocuments
|
||||
id="test"
|
||||
|
@ -63,20 +63,20 @@ const renderCompareDocuments = ({
|
|||
dataView={dataViewWithTimefieldMock}
|
||||
isPlainRecord={false}
|
||||
selectedFieldNames={['message', 'extension', 'bytes']}
|
||||
selectedDocs={['0', '1', '2']}
|
||||
selectedDocIds={['0', '1', '2']}
|
||||
schemaDetectors={[]}
|
||||
forceShowAllFields={forceShowAllFields}
|
||||
showFullScreenButton={true}
|
||||
fieldFormats={{} as any}
|
||||
getDocById={getDocById}
|
||||
setSelectedDocs={setSelectedDocs}
|
||||
replaceSelectedDocs={replaceSelectedDocs}
|
||||
setIsCompareActive={jest.fn()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
const { rerender } = render(getCompareDocuments());
|
||||
return {
|
||||
setSelectedDocs,
|
||||
replaceSelectedDocs,
|
||||
rerender: (props?: Partial<CompareDocumentsProps>) => rerender(getCompareDocuments(props)),
|
||||
};
|
||||
};
|
||||
|
@ -146,10 +146,10 @@ describe('CompareDocuments', () => {
|
|||
});
|
||||
|
||||
it('should set selected docs when columns change', () => {
|
||||
const { setSelectedDocs } = renderCompareDocuments();
|
||||
const { replaceSelectedDocs } = renderCompareDocuments();
|
||||
const visibleColumns = ['fields_generated-id', '0', '1', '2'];
|
||||
mockDataGridProps?.columnVisibility.setVisibleColumns(visibleColumns);
|
||||
expect(setSelectedDocs).toHaveBeenCalledWith(visibleColumns.slice(1));
|
||||
expect(replaceSelectedDocs).toHaveBeenCalledWith(visibleColumns.slice(1));
|
||||
});
|
||||
|
||||
it('should force show all fields when prop is true', () => {
|
||||
|
|
|
@ -42,13 +42,13 @@ export interface CompareDocumentsProps {
|
|||
dataView: DataView;
|
||||
isPlainRecord: boolean;
|
||||
selectedFieldNames: string[];
|
||||
selectedDocs: string[];
|
||||
selectedDocIds: string[];
|
||||
schemaDetectors: EuiDataGridSchemaDetector[];
|
||||
forceShowAllFields: boolean;
|
||||
showFullScreenButton?: boolean;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
getDocById: (id: string) => DataTableRecord | undefined;
|
||||
setSelectedDocs: (selectedDocs: string[]) => void;
|
||||
replaceSelectedDocs: (docIds: string[]) => void;
|
||||
setIsCompareActive: (isCompareActive: boolean) => void;
|
||||
additionalFieldGroups?: AdditionalFieldGroups;
|
||||
}
|
||||
|
@ -69,13 +69,13 @@ const CompareDocuments = ({
|
|||
isPlainRecord,
|
||||
selectedFieldNames,
|
||||
additionalFieldGroups,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
schemaDetectors,
|
||||
forceShowAllFields,
|
||||
showFullScreenButton,
|
||||
fieldFormats,
|
||||
getDocById,
|
||||
setSelectedDocs,
|
||||
replaceSelectedDocs,
|
||||
setIsCompareActive,
|
||||
}: CompareDocumentsProps) => {
|
||||
// Memoize getDocById to ensure we don't lose access to the comparison docs if, for example,
|
||||
|
@ -104,7 +104,7 @@ const CompareDocuments = ({
|
|||
dataView,
|
||||
selectedFieldNames,
|
||||
additionalFieldGroups,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
showAllFields: Boolean(forceShowAllFields || showAllFields),
|
||||
showMatchingValues: Boolean(showMatchingValues),
|
||||
getDocById: memoizedGetDocById,
|
||||
|
@ -113,25 +113,25 @@ const CompareDocuments = ({
|
|||
wrapper,
|
||||
isPlainRecord,
|
||||
fieldColumnId,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
getDocById: memoizedGetDocById,
|
||||
setSelectedDocs,
|
||||
replaceSelectedDocs,
|
||||
});
|
||||
const comparisonColumnVisibility = useMemo<EuiDataGridColumnVisibility>(
|
||||
() => ({
|
||||
visibleColumns: comparisonColumns.map(({ id: columnId }) => columnId),
|
||||
setVisibleColumns: (visibleColumns) => {
|
||||
const [_fieldColumnId, ...newSelectedDocs] = visibleColumns;
|
||||
setSelectedDocs(newSelectedDocs);
|
||||
replaceSelectedDocs(newSelectedDocs);
|
||||
},
|
||||
}),
|
||||
[comparisonColumns, setSelectedDocs]
|
||||
[comparisonColumns, replaceSelectedDocs]
|
||||
);
|
||||
const additionalControls = useMemo(
|
||||
() => (
|
||||
<ComparisonControls
|
||||
isPlainRecord={isPlainRecord}
|
||||
selectedDocs={selectedDocs}
|
||||
selectedDocIds={selectedDocIds}
|
||||
showDiff={showDiff}
|
||||
diffMode={diffMode}
|
||||
showDiffDecorations={showDiffDecorations}
|
||||
|
@ -150,7 +150,7 @@ const CompareDocuments = ({
|
|||
diffMode,
|
||||
forceShowAllFields,
|
||||
isPlainRecord,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
setDiffMode,
|
||||
setIsCompareActive,
|
||||
setShowAllFields,
|
||||
|
@ -184,7 +184,7 @@ const CompareDocuments = ({
|
|||
dataView,
|
||||
comparisonFields,
|
||||
fieldColumnId,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
diffMode: showDiff ? diffMode : undefined,
|
||||
fieldFormats,
|
||||
getDocById: memoizedGetDocById,
|
||||
|
|
|
@ -20,7 +20,7 @@ const renderComparisonControls = ({
|
|||
isPlainRecord?: ComparisonControlsProps['isPlainRecord'];
|
||||
forceShowAllFields?: ComparisonControlsProps['forceShowAllFields'];
|
||||
} = {}) => {
|
||||
const selectedDocs = ['0', '1', '2'];
|
||||
const selectedDocIds = ['0', '1', '2'];
|
||||
const Wrapper = () => {
|
||||
const [showDiff, setShowDiff] = useState(true);
|
||||
const [diffMode, setDiffMode] = useState<DocumentDiffMode>('basic');
|
||||
|
@ -34,7 +34,7 @@ const renderComparisonControls = ({
|
|||
<IntlProvider locale="en">
|
||||
<ComparisonControls
|
||||
isPlainRecord={isPlainRecord}
|
||||
selectedDocs={selectedDocs}
|
||||
selectedDocIds={selectedDocIds}
|
||||
showDiff={showDiff}
|
||||
diffMode={diffMode}
|
||||
showDiffDecorations={showDiffDecorations}
|
||||
|
@ -67,7 +67,7 @@ const renderComparisonControls = ({
|
|||
return {
|
||||
getComparisonCountDisplay: () =>
|
||||
screen.getByText(
|
||||
`Comparing ${selectedDocs.length} ${isPlainRecord ? 'results' : 'documents'}`
|
||||
`Comparing ${selectedDocIds.length} ${isPlainRecord ? 'results' : 'documents'}`
|
||||
),
|
||||
getComparisonSettingsButton,
|
||||
clickComparisonSettingsButton: () => userEvent.click(getComparisonSettingsButton()),
|
||||
|
|
|
@ -30,7 +30,7 @@ import type { DocumentDiffMode } from './types';
|
|||
|
||||
export interface ComparisonControlsProps {
|
||||
isPlainRecord?: boolean;
|
||||
selectedDocs: string[];
|
||||
selectedDocIds: string[];
|
||||
showDiff: boolean | undefined;
|
||||
diffMode: DocumentDiffMode | undefined;
|
||||
showDiffDecorations: boolean | undefined;
|
||||
|
@ -47,7 +47,7 @@ export interface ComparisonControlsProps {
|
|||
|
||||
export const ComparisonControls = ({
|
||||
isPlainRecord,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
showDiff,
|
||||
diffMode,
|
||||
showDiffDecorations,
|
||||
|
@ -72,13 +72,13 @@ export const ComparisonControls = ({
|
|||
<FormattedMessage
|
||||
id="unifiedDataTable.comparingResults"
|
||||
defaultMessage="Comparing {documentCount} results"
|
||||
values={{ documentCount: selectedDocs.length }}
|
||||
values={{ documentCount: selectedDocIds.length }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="unifiedDataTable.comparingDocuments"
|
||||
defaultMessage="Comparing {documentCount} documents"
|
||||
values={{ documentCount: selectedDocs.length }}
|
||||
values={{ documentCount: selectedDocIds.length }}
|
||||
/>
|
||||
)}
|
||||
</strong>
|
||||
|
|
|
@ -55,7 +55,7 @@ const renderComparisonCellValue = (props: Partial<UseComparisonCellValueProps> =
|
|||
dataView: dataViewWithTimefieldMock,
|
||||
comparisonFields: ['message', 'extension', 'bytes'],
|
||||
fieldColumnId,
|
||||
selectedDocs: ['0', '1', '2'],
|
||||
selectedDocIds: ['0', '1', '2'],
|
||||
diffMode: undefined,
|
||||
fieldFormats: fieldFormatsMock,
|
||||
getDocById,
|
||||
|
@ -408,7 +408,7 @@ describe('useComparisonCellValue', () => {
|
|||
expect(calculateDiff).toHaveBeenCalledTimes(2);
|
||||
renderComparisonCell(cellProps2);
|
||||
expect(calculateDiff).toHaveBeenCalledTimes(2);
|
||||
rerender({ diffMode: 'words', selectedDocs: ['1', '2', '0'] });
|
||||
rerender({ diffMode: 'words', selectedDocIds: ['1', '2', '0'] });
|
||||
const cellProps3 = {
|
||||
...cellProps1,
|
||||
columnId: '2',
|
||||
|
@ -425,7 +425,7 @@ describe('useComparisonCellValue', () => {
|
|||
expect(calculateDiff).toHaveBeenCalledTimes(4);
|
||||
renderComparisonCell(cellProps4);
|
||||
expect(calculateDiff).toHaveBeenCalledTimes(4);
|
||||
rerender({ diffMode: 'lines', selectedDocs: ['2', '0', '1'] });
|
||||
rerender({ diffMode: 'lines', selectedDocIds: ['2', '0', '1'] });
|
||||
const cellProps5 = {
|
||||
...cellProps1,
|
||||
columnId: '0',
|
||||
|
|
|
@ -39,7 +39,7 @@ export interface UseComparisonCellValueProps {
|
|||
dataView: DataView;
|
||||
comparisonFields: string[];
|
||||
fieldColumnId: string;
|
||||
selectedDocs: string[];
|
||||
selectedDocIds: string[];
|
||||
diffMode: DocumentDiffMode | undefined;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
getDocById: (id: string) => DataTableRecord | undefined;
|
||||
|
@ -50,13 +50,13 @@ export const useComparisonCellValue = ({
|
|||
dataView,
|
||||
comparisonFields,
|
||||
fieldColumnId,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
diffMode,
|
||||
fieldFormats,
|
||||
getDocById,
|
||||
additionalFieldGroups,
|
||||
}: UseComparisonCellValueProps) => {
|
||||
const baseDocId = selectedDocs[0];
|
||||
const baseDocId = selectedDocIds[0];
|
||||
const baseDoc = useMemo(() => getDocById(baseDocId)?.flattened, [baseDocId, getDocById]);
|
||||
const [calculateDiffMemoized] = useState(() => createCalculateDiffMemoized());
|
||||
|
||||
|
@ -92,7 +92,7 @@ export const useComparisonCellValue = ({
|
|||
);
|
||||
};
|
||||
|
||||
type CellValueProps = Omit<UseComparisonCellValueProps, 'selectedDocs'> &
|
||||
type CellValueProps = Omit<UseComparisonCellValueProps, 'selectedDocIds'> &
|
||||
EuiDataGridCellValueElementProps & {
|
||||
baseDocId: string;
|
||||
baseDoc: DataTableRecord['flattened'] | undefined;
|
||||
|
|
|
@ -78,7 +78,7 @@ const docs = generateEsHits(dataViewWithTimefieldMock, 4).map((hit) =>
|
|||
const defaultGetDocById = (id: string) => docs.find((doc) => doc.raw._id === id);
|
||||
|
||||
const fieldColumnId = 'fieldColumnId';
|
||||
const selectedDocs = ['0', '1', '2', '3'];
|
||||
const selectedDocIds = ['0', '1', '2', '3'];
|
||||
|
||||
const renderColumns = ({
|
||||
wrapperWidth,
|
||||
|
@ -93,7 +93,7 @@ const renderColumns = ({
|
|||
if (wrapperWidth) {
|
||||
Object.defineProperty(wrapper, 'offsetWidth', { value: wrapperWidth });
|
||||
}
|
||||
const setSelectedDocs = jest.fn();
|
||||
const replaceSelectedDocs = jest.fn();
|
||||
const {
|
||||
result: { current: columns },
|
||||
} = renderHook(() =>
|
||||
|
@ -101,17 +101,17 @@ const renderColumns = ({
|
|||
wrapper,
|
||||
isPlainRecord,
|
||||
fieldColumnId,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
getDocById,
|
||||
setSelectedDocs,
|
||||
replaceSelectedDocs,
|
||||
})
|
||||
);
|
||||
return { columns, setSelectedDocs };
|
||||
return { columns, replaceSelectedDocs };
|
||||
};
|
||||
|
||||
describe('useComparisonColumns', () => {
|
||||
it('should return comparison columns', () => {
|
||||
const { columns, setSelectedDocs } = renderColumns();
|
||||
const { columns, replaceSelectedDocs } = renderColumns();
|
||||
expect(columns).toEqual([
|
||||
{
|
||||
id: fieldColumnId,
|
||||
|
@ -123,17 +123,17 @@ describe('useComparisonColumns', () => {
|
|||
},
|
||||
getComparisonColumn({
|
||||
column: {
|
||||
id: selectedDocs[0],
|
||||
id: selectedDocIds[0],
|
||||
display: expect.anything(),
|
||||
displayAsText: `Pinned document: ${selectedDocs[0]}`,
|
||||
displayAsText: `Pinned document: ${selectedDocIds[0]}`,
|
||||
},
|
||||
includeRemoveAction: true,
|
||||
}),
|
||||
getComparisonColumn({
|
||||
column: {
|
||||
id: selectedDocs[1],
|
||||
display: selectedDocs[1],
|
||||
displayAsText: `Comparison document: ${selectedDocs[1]}`,
|
||||
id: selectedDocIds[1],
|
||||
display: selectedDocIds[1],
|
||||
displayAsText: `Comparison document: ${selectedDocIds[1]}`,
|
||||
actions: {
|
||||
showMoveRight: true,
|
||||
},
|
||||
|
@ -143,9 +143,9 @@ describe('useComparisonColumns', () => {
|
|||
}),
|
||||
getComparisonColumn({
|
||||
column: {
|
||||
id: selectedDocs[2],
|
||||
display: selectedDocs[2],
|
||||
displayAsText: `Comparison document: ${selectedDocs[2]}`,
|
||||
id: selectedDocIds[2],
|
||||
display: selectedDocIds[2],
|
||||
displayAsText: `Comparison document: ${selectedDocIds[2]}`,
|
||||
actions: {
|
||||
showMoveLeft: true,
|
||||
showMoveRight: true,
|
||||
|
@ -156,9 +156,9 @@ describe('useComparisonColumns', () => {
|
|||
}),
|
||||
getComparisonColumn({
|
||||
column: {
|
||||
id: selectedDocs[3],
|
||||
display: selectedDocs[3],
|
||||
displayAsText: `Comparison document: ${selectedDocs[3]}`,
|
||||
id: selectedDocIds[3],
|
||||
display: selectedDocIds[3],
|
||||
displayAsText: `Comparison document: ${selectedDocIds[3]}`,
|
||||
actions: {
|
||||
showMoveLeft: true,
|
||||
},
|
||||
|
@ -192,17 +192,17 @@ describe('useComparisonColumns', () => {
|
|||
const removeAction = actions.additional?.[1].onClick;
|
||||
render(<button onClick={pinAction} data-test-subj="pin" />);
|
||||
userEvent.click(screen.getByTestId('pin'));
|
||||
expect(setSelectedDocs).toHaveBeenCalledTimes(1);
|
||||
expect(setSelectedDocs).toHaveBeenLastCalledWith(['1', '0', '2', '3']);
|
||||
expect(replaceSelectedDocs).toHaveBeenCalledTimes(1);
|
||||
expect(replaceSelectedDocs).toHaveBeenLastCalledWith(['1', '0', '2', '3']);
|
||||
render(<button onClick={removeAction} data-test-subj="remove" />);
|
||||
userEvent.click(screen.getByTestId('remove'));
|
||||
expect(setSelectedDocs).toHaveBeenCalledTimes(2);
|
||||
expect(setSelectedDocs).toHaveBeenLastCalledWith(['0', '2', '3']);
|
||||
expect(replaceSelectedDocs).toHaveBeenCalledTimes(2);
|
||||
expect(replaceSelectedDocs).toHaveBeenLastCalledWith(['0', '2', '3']);
|
||||
});
|
||||
|
||||
it('should not set column widths if there is sufficient space', () => {
|
||||
const { columns } = renderColumns({
|
||||
wrapperWidth: FIELD_COLUMN_WIDTH + selectedDocs.length * DEFAULT_COLUMN_WIDTH,
|
||||
wrapperWidth: FIELD_COLUMN_WIDTH + selectedDocIds.length * DEFAULT_COLUMN_WIDTH,
|
||||
});
|
||||
expect(columns[0].initialWidth).toBe(FIELD_COLUMN_WIDTH);
|
||||
expect(columns[1].initialWidth).toBe(undefined);
|
||||
|
@ -220,11 +220,12 @@ describe('useComparisonColumns', () => {
|
|||
});
|
||||
|
||||
it('should skip columns for missing docs', () => {
|
||||
const getDocById = (id: string) => (id === selectedDocs[1] ? undefined : defaultGetDocById(id));
|
||||
const getDocById = (id: string) =>
|
||||
id === selectedDocIds[1] ? undefined : defaultGetDocById(id);
|
||||
const { columns } = renderColumns({ getDocById });
|
||||
expect(columns).toHaveLength(4);
|
||||
expect(columns[1].displayAsText).toBe(`Pinned document: ${selectedDocs[0]}`);
|
||||
expect(columns[2].displayAsText).toBe(`Comparison document: ${selectedDocs[2]}`);
|
||||
expect(columns[3].displayAsText).toBe(`Comparison document: ${selectedDocs[3]}`);
|
||||
expect(columns[1].displayAsText).toBe(`Pinned document: ${selectedDocIds[0]}`);
|
||||
expect(columns[2].displayAsText).toBe(`Comparison document: ${selectedDocIds[2]}`);
|
||||
expect(columns[3].displayAsText).toBe(`Comparison document: ${selectedDocIds[3]}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,9 +22,9 @@ export interface UseComparisonColumnsProps {
|
|||
wrapper: HTMLElement | null;
|
||||
isPlainRecord: boolean;
|
||||
fieldColumnId: string;
|
||||
selectedDocs: string[];
|
||||
selectedDocIds: string[];
|
||||
getDocById: (docId: string) => DataTableRecord | undefined;
|
||||
setSelectedDocs: (selectedDocs: string[]) => void;
|
||||
replaceSelectedDocs: (docIds: string[]) => void;
|
||||
}
|
||||
|
||||
export const DEFAULT_COLUMN_WIDTH = 300;
|
||||
|
@ -37,9 +37,9 @@ export const useComparisonColumns = ({
|
|||
wrapper,
|
||||
isPlainRecord,
|
||||
fieldColumnId,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
getDocById,
|
||||
setSelectedDocs,
|
||||
replaceSelectedDocs,
|
||||
}: UseComparisonColumnsProps) => {
|
||||
const comparisonColumns = useMemo<EuiDataGridColumn[]>(() => {
|
||||
const fieldsColumn: EuiDataGridColumn = {
|
||||
|
@ -54,11 +54,11 @@ export const useComparisonColumns = ({
|
|||
const currentColumns = [fieldsColumn];
|
||||
const wrapperWidth = wrapper?.offsetWidth ?? 0;
|
||||
const columnWidth =
|
||||
DEFAULT_COLUMN_WIDTH * selectedDocs.length + FIELD_COLUMN_WIDTH > wrapperWidth
|
||||
DEFAULT_COLUMN_WIDTH * selectedDocIds.length + FIELD_COLUMN_WIDTH > wrapperWidth
|
||||
? DEFAULT_COLUMN_WIDTH
|
||||
: undefined;
|
||||
|
||||
selectedDocs.forEach((docId, docIndex) => {
|
||||
selectedDocIds.forEach((docId, docIndex) => {
|
||||
const doc = getDocById(docId);
|
||||
|
||||
if (!doc) {
|
||||
|
@ -75,19 +75,19 @@ export const useComparisonColumns = ({
|
|||
}),
|
||||
size: 'xs',
|
||||
onClick: () => {
|
||||
const newSelectedDocs = [...selectedDocs];
|
||||
const newSelectedDocs = [...selectedDocIds];
|
||||
const index = newSelectedDocs.indexOf(docId);
|
||||
const [baseDocId] = newSelectedDocs;
|
||||
|
||||
newSelectedDocs[0] = docId;
|
||||
newSelectedDocs[index] = baseDocId;
|
||||
|
||||
setSelectedDocs(newSelectedDocs);
|
||||
replaceSelectedDocs(newSelectedDocs);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedDocs.length > 2) {
|
||||
if (selectedDocIds.length > 2) {
|
||||
additional.push({
|
||||
iconType: 'cross',
|
||||
label: i18n.translate('unifiedDataTable.removeFromComparison', {
|
||||
|
@ -95,7 +95,7 @@ export const useComparisonColumns = ({
|
|||
}),
|
||||
size: 'xs',
|
||||
onClick: () => {
|
||||
setSelectedDocs(selectedDocs.filter((id) => id !== docId));
|
||||
replaceSelectedDocs(selectedDocIds.filter((id) => id !== docId));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ export const useComparisonColumns = ({
|
|||
actions: {
|
||||
showHide: false,
|
||||
showMoveLeft: docIndex > 1,
|
||||
showMoveRight: docIndex > 0 && docIndex < selectedDocs.length - 1,
|
||||
showMoveRight: docIndex > 0 && docIndex < selectedDocIds.length - 1,
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
additional,
|
||||
|
@ -164,8 +164,8 @@ export const useComparisonColumns = ({
|
|||
fieldColumnId,
|
||||
getDocById,
|
||||
isPlainRecord,
|
||||
selectedDocs,
|
||||
setSelectedDocs,
|
||||
selectedDocIds,
|
||||
replaceSelectedDocs,
|
||||
wrapper?.offsetWidth,
|
||||
]);
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ const renderFields = ({
|
|||
useComparisonFields({
|
||||
dataView,
|
||||
selectedFieldNames: ['message', 'extension', 'bytes'],
|
||||
selectedDocs: ['0', '1', '2'],
|
||||
selectedDocIds: ['0', '1', '2'],
|
||||
showAllFields: true,
|
||||
showMatchingValues: true,
|
||||
getDocById,
|
||||
|
|
|
@ -17,7 +17,7 @@ export const MAX_COMPARISON_FIELDS = 250;
|
|||
export interface UseComparisonFieldsProps {
|
||||
dataView: DataView;
|
||||
selectedFieldNames: string[];
|
||||
selectedDocs: string[];
|
||||
selectedDocIds: string[];
|
||||
showAllFields: boolean;
|
||||
showMatchingValues: boolean;
|
||||
getDocById: (id: string) => DataTableRecord | undefined;
|
||||
|
@ -27,14 +27,14 @@ export interface UseComparisonFieldsProps {
|
|||
export const useComparisonFields = ({
|
||||
dataView,
|
||||
selectedFieldNames,
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
showAllFields,
|
||||
showMatchingValues,
|
||||
getDocById,
|
||||
additionalFieldGroups,
|
||||
}: UseComparisonFieldsProps) => {
|
||||
const { baseDoc, comparisonDocs } = useMemo(() => {
|
||||
const [baseDocId, ...comparisonDocIds] = selectedDocs;
|
||||
const [baseDocId, ...comparisonDocIds] = selectedDocIds;
|
||||
|
||||
return {
|
||||
baseDoc: getDocById(baseDocId),
|
||||
|
@ -42,7 +42,7 @@ export const useComparisonFields = ({
|
|||
.map((docId) => getDocById(docId))
|
||||
.filter((doc): doc is DataTableRecord => Boolean(doc)),
|
||||
};
|
||||
}, [getDocById, selectedDocs]);
|
||||
}, [getDocById, selectedDocIds]);
|
||||
|
||||
return useMemo(() => {
|
||||
let comparisonFields = convertFieldsToFallbackFields({
|
||||
|
|
|
@ -87,9 +87,7 @@ exports[`renderCustomToolbar should render correctly with an element 1`] = `
|
|||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="unifiedDataTableToolbarControlButton"
|
||||
>
|
||||
<div>
|
||||
additional
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
|
@ -190,9 +188,7 @@ exports[`renderCustomToolbar should render successfully 1`] = `
|
|||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="unifiedDataTableToolbarControlButton"
|
||||
>
|
||||
<div>
|
||||
additional
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -1,58 +1,68 @@
|
|||
.unifiedDataTableToolbar {
|
||||
padding: $euiSizeS $euiSizeS $euiSizeXS;
|
||||
}
|
||||
|
||||
.unifiedDataTableToolbarControlButton .euiDataGridToolbarControl {
|
||||
block-size: $euiSizeXL;
|
||||
border: $euiBorderThin;
|
||||
border-radius: $euiBorderRadiusSmall;
|
||||
.unifiedDataTableToolbarControlButton .euiDataGridToolbarControl {
|
||||
block-size: $euiSizeXL;
|
||||
border: $euiBorderThin;
|
||||
border-radius: $euiBorderRadiusSmall;
|
||||
|
||||
// making the icons larger than the default size
|
||||
& svg {
|
||||
inline-size: $euiSize;
|
||||
block-size: $euiSize;
|
||||
// making the icons larger than the default size
|
||||
& svg {
|
||||
inline-size: $euiSize;
|
||||
block-size: $euiSize;
|
||||
}
|
||||
|
||||
// cancel default background changes
|
||||
&:active, &:focus {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// add toolbar control animation
|
||||
transition: transform $euiAnimSpeedNormal ease-in-out;
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// cancel default background changes
|
||||
&:active, &:focus {
|
||||
background: transparent;
|
||||
.unifiedDataTableToolbarControlGroup {
|
||||
box-shadow: inset 0 0 0 $euiBorderWidthThin $euiBorderColor;
|
||||
border-radius: $euiBorderRadiusSmall;
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
flex-direction: row;
|
||||
|
||||
& .unifiedDataTableToolbarControlButton .euiDataGridToolbarControl {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
& .unifiedDataTableToolbarControlButton + .unifiedDataTableToolbarControlButton {
|
||||
border-inline-start: $euiBorderThin;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// add toolbar control animation
|
||||
transition: transform $euiAnimSpeedNormal ease-in-out;
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
.unifiedDataTableToolbarControlIconButton .euiButtonIcon {
|
||||
inline-size: $euiSizeXL;
|
||||
block-size: $euiSizeXL;
|
||||
|
||||
// cancel default behaviour
|
||||
&:hover, &:active, &:focus {
|
||||
background: transparent;
|
||||
animation: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.unifiedDataTableToolbarControlIconButton + & {
|
||||
border-inline-start: $euiBorderThin;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
|
||||
.unifiedDataTableToolbarBottom {
|
||||
position: relative; // for placing a loading indicator correctly
|
||||
}
|
||||
}
|
||||
|
||||
.unifiedDataTableToolbarControlGroup {
|
||||
box-shadow: inset 0 0 0 $euiBorderWidthThin $euiBorderColor;
|
||||
border-radius: $euiBorderRadiusSmall;
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.unifiedDataTableToolbarControlIconButton .euiButtonIcon {
|
||||
inline-size: $euiSizeXL;
|
||||
block-size: $euiSizeXL;
|
||||
|
||||
// cancel default behaviour
|
||||
&:hover, &:active, &:focus {
|
||||
background: transparent;
|
||||
animation: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.unifiedDataTableToolbarControlIconButton + & {
|
||||
border-inline-start: $euiBorderThin;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.unifiedDataTableToolbarBottom {
|
||||
position: relative; // for placing a loading indicator correctly
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ export const internalRenderCustomToolbar = (
|
|||
<>
|
||||
{leftSide && additionalControls && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<div className="unifiedDataTableToolbarControlButton">{additionalControls}</div>
|
||||
<div>{additionalControls}</div>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{columnControl && (
|
||||
|
@ -67,7 +67,7 @@ export const internalRenderCustomToolbar = (
|
|||
)}
|
||||
{!leftSide && additionalControls && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<div className="unifiedDataTableToolbarControlButton">{additionalControls}</div>
|
||||
<div>{additionalControls}</div>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -60,6 +60,11 @@
|
|||
padding-right: 0;
|
||||
}
|
||||
|
||||
.euiDataGridHeaderCell.euiDataGridHeaderCell--controlColumn[data-gridcell-column-id='select'] {
|
||||
padding-left: $euiSizeXS;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.euiDataGridHeaderCell.euiDataGridHeaderCell--controlColumn[data-gridcell-column-id='colorIndicator'],
|
||||
.euiDataGridRowCell.euiDataGridRowCell--controlColumn[data-gridcell-column-id='colorIndicator'] {
|
||||
padding: 0;
|
||||
|
|
|
@ -691,7 +691,7 @@ describe('UnifiedDataTable', () => {
|
|||
// additional controls become available after selecting a document
|
||||
act(() => {
|
||||
component
|
||||
.find('[data-gridcell-column-id="select"] .euiCheckbox__input')
|
||||
.find('.euiDataGridRowCell[data-gridcell-column-id="select"] .euiCheckbox__input')
|
||||
.first()
|
||||
.simulate('change');
|
||||
});
|
||||
|
@ -767,17 +767,28 @@ describe('UnifiedDataTable', () => {
|
|||
);
|
||||
};
|
||||
|
||||
const getSelectedDocumentsButton = () => screen.queryByRole('button', { name: /Selected/ });
|
||||
const getSelectedDocumentsButton = () => screen.queryByTestId('unifiedDataTableSelectionBtn');
|
||||
|
||||
const selectDocument = (document: EsHitRecord) =>
|
||||
userEvent.click(screen.getByTestId(`dscGridSelectDoc-${getDocId(document)}`));
|
||||
|
||||
const getCompareDocumentsButton = () => screen.queryByRole('button', { name: /Compare/ });
|
||||
const openSelectedRowsMenu = async () => {
|
||||
userEvent.click(await screen.findByTestId('unifiedDataTableSelectionBtn'));
|
||||
await screen.findAllByText('Clear selection');
|
||||
};
|
||||
|
||||
const closeSelectedRowsMenu = async () => {
|
||||
userEvent.click(await screen.findByTestId('unifiedDataTableSelectionBtn'));
|
||||
};
|
||||
|
||||
const getCompareDocumentsButton = () =>
|
||||
screen.queryByTestId('unifiedDataTableCompareSelectedDocuments');
|
||||
|
||||
const goToComparisonMode = async () => {
|
||||
selectDocument(esHitsMock[0]);
|
||||
selectDocument(esHitsMock[1]);
|
||||
userEvent.click(getCompareDocumentsButton()!);
|
||||
await openSelectedRowsMenu();
|
||||
userEvent.click(await screen.findByTestId('unifiedDataTableCompareSelectedDocuments'));
|
||||
await screen.findByText('Comparing 2 documents');
|
||||
};
|
||||
|
||||
|
@ -796,22 +807,28 @@ describe('UnifiedDataTable', () => {
|
|||
const getCellValues = () =>
|
||||
Array.from(document.querySelectorAll(`.${CELL_CLASS}`)).map(({ textContent }) => textContent);
|
||||
|
||||
it('should not allow comparison if less than 2 documents are selected', () => {
|
||||
it('should not allow comparison if less than 2 documents are selected', async () => {
|
||||
renderDataTable({ enableComparisonMode: true });
|
||||
expect(getSelectedDocumentsButton()).not.toBeInTheDocument();
|
||||
selectDocument(esHitsMock[0]);
|
||||
expect(getSelectedDocumentsButton()).toBeInTheDocument();
|
||||
await openSelectedRowsMenu();
|
||||
expect(getCompareDocumentsButton()).not.toBeInTheDocument();
|
||||
await closeSelectedRowsMenu();
|
||||
selectDocument(esHitsMock[1]);
|
||||
expect(getSelectedDocumentsButton()).toBeInTheDocument();
|
||||
await openSelectedRowsMenu();
|
||||
expect(getCompareDocumentsButton()).toBeInTheDocument();
|
||||
await closeSelectedRowsMenu();
|
||||
});
|
||||
|
||||
it('should not allow comparison if comparison mode is disabled', () => {
|
||||
it('should not allow comparison if comparison mode is disabled', async () => {
|
||||
renderDataTable({ enableComparisonMode: false });
|
||||
selectDocument(esHitsMock[0]);
|
||||
selectDocument(esHitsMock[1]);
|
||||
await openSelectedRowsMenu();
|
||||
expect(getCompareDocumentsButton()).not.toBeInTheDocument();
|
||||
await closeSelectedRowsMenu();
|
||||
});
|
||||
|
||||
it('should allow comparison if 2 or more documents are selected and comparison mode is enabled', async () => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState, useRef, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { of } from 'rxjs';
|
||||
|
@ -29,8 +29,6 @@ import {
|
|||
EuiDataGridToolBarVisibilityDisplaySelectorOptions,
|
||||
EuiDataGridStyle,
|
||||
EuiDataGridProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
|
@ -68,10 +66,7 @@ import {
|
|||
} from './data_table_columns';
|
||||
import { UnifiedDataTableContext } from '../table_context';
|
||||
import { getSchemaDetectors } from './data_table_schema';
|
||||
import {
|
||||
DataTableCompareToolbarBtn,
|
||||
DataTableDocumentToolbarBtn,
|
||||
} from './data_table_document_selection';
|
||||
import { DataTableDocumentToolbarBtn } from './data_table_document_selection';
|
||||
import { useRowHeightsOptions } from '../hooks/use_row_heights_options';
|
||||
import {
|
||||
DEFAULT_ROWS_PER_PAGE,
|
||||
|
@ -86,6 +81,7 @@ import { CompareDocuments } from './compare_documents';
|
|||
import { useFullScreenWatcher } from '../hooks/use_full_screen_watcher';
|
||||
import { UnifiedDataTableRenderCustomToolbar } from './custom_toolbar/render_custom_toolbar';
|
||||
import { getCustomCellPopoverRenderer } from '../utils/get_render_cell_popover';
|
||||
import { useSelectedDocs } from '../hooks/use_selected_docs';
|
||||
import {
|
||||
getColorIndicatorControlColumn,
|
||||
type ColorIndicatorControlColumnParams,
|
||||
|
@ -472,39 +468,35 @@ export const UnifiedDataTable = ({
|
|||
services;
|
||||
const { darkMode } = useObservable(services.theme?.theme$ ?? of(themeDefault), themeDefault);
|
||||
const dataGridRef = useRef<EuiDataGridRefProps>(null);
|
||||
const [selectedDocs, setSelectedDocs] = useState<string[]>([]);
|
||||
const [isFilterActive, setIsFilterActive] = useState(false);
|
||||
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 usedSelectedDocs = useMemo(() => {
|
||||
if (!selectedDocs.length || !rows?.length) {
|
||||
return [];
|
||||
}
|
||||
// filter out selected docs that are no longer part of the current data
|
||||
const result = selectedDocs.filter((docId) => !!getDocById(docId));
|
||||
if (result.length === 0 && isFilterActive) {
|
||||
const selectedDocsState = useSelectedDocs(docMap);
|
||||
const { isDocSelected, hasSelectedDocs, selectedDocIds, replaceSelectedDocs } = selectedDocsState;
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSelectedDocs && isFilterActive) {
|
||||
setIsFilterActive(false);
|
||||
}
|
||||
return result;
|
||||
}, [selectedDocs, rows?.length, isFilterActive, getDocById]);
|
||||
}, [isFilterActive, hasSelectedDocs, setIsFilterActive]);
|
||||
|
||||
const displayedRows = useMemo(() => {
|
||||
if (!rows) {
|
||||
return [];
|
||||
}
|
||||
if (!isFilterActive || usedSelectedDocs.length === 0) {
|
||||
if (!isFilterActive || !hasSelectedDocs) {
|
||||
return rows;
|
||||
}
|
||||
const rowsFiltered = rows.filter((row) => usedSelectedDocs.includes(row.id));
|
||||
const rowsFiltered = rows.filter((row) => isDocSelected(row.id));
|
||||
if (!rowsFiltered.length) {
|
||||
// in case the selected docs are no longer part of the sample of 500, show all docs
|
||||
return rows;
|
||||
}
|
||||
return rowsFiltered;
|
||||
}, [rows, usedSelectedDocs, isFilterActive]);
|
||||
}, [rows, isFilterActive, hasSelectedDocs, isDocSelected]);
|
||||
|
||||
const valueToStringConverter: ValueToStringConverter = useCallback(
|
||||
(rowIndex, columnId, options) => {
|
||||
|
@ -520,40 +512,6 @@ export const UnifiedDataTable = ({
|
|||
[displayedRows, dataView, fieldFormats]
|
||||
);
|
||||
|
||||
const unifiedDataTableContextValue = useMemo(
|
||||
() => ({
|
||||
expanded: expandedDoc,
|
||||
setExpanded: setExpandedDoc,
|
||||
rows: displayedRows,
|
||||
onFilter,
|
||||
dataView,
|
||||
isDarkMode: darkMode,
|
||||
selectedDocs: usedSelectedDocs,
|
||||
setSelectedDocs: (newSelectedDocs: React.SetStateAction<string[]>) => {
|
||||
setSelectedDocs(newSelectedDocs);
|
||||
if (isFilterActive && newSelectedDocs.length === 0) {
|
||||
setIsFilterActive(false);
|
||||
}
|
||||
},
|
||||
valueToStringConverter,
|
||||
componentsTourSteps,
|
||||
isPlainRecord,
|
||||
}),
|
||||
[
|
||||
componentsTourSteps,
|
||||
darkMode,
|
||||
dataView,
|
||||
isPlainRecord,
|
||||
displayedRows,
|
||||
expandedDoc,
|
||||
isFilterActive,
|
||||
onFilter,
|
||||
setExpandedDoc,
|
||||
usedSelectedDocs,
|
||||
valueToStringConverter,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Pagination
|
||||
*/
|
||||
|
@ -605,6 +563,36 @@ export const UnifiedDataTable = ({
|
|||
);
|
||||
}, [currentPageSize, setPagination]);
|
||||
|
||||
const unifiedDataTableContextValue = useMemo(
|
||||
() => ({
|
||||
expanded: expandedDoc,
|
||||
setExpanded: setExpandedDoc,
|
||||
rows: displayedRows,
|
||||
onFilter,
|
||||
dataView,
|
||||
isDarkMode: darkMode,
|
||||
selectedDocsState,
|
||||
valueToStringConverter,
|
||||
componentsTourSteps,
|
||||
isPlainRecord,
|
||||
pageIndex: paginationObj?.pageIndex,
|
||||
pageSize: paginationObj?.pageSize,
|
||||
}),
|
||||
[
|
||||
componentsTourSteps,
|
||||
darkMode,
|
||||
dataView,
|
||||
isPlainRecord,
|
||||
displayedRows,
|
||||
expandedDoc,
|
||||
onFilter,
|
||||
setExpandedDoc,
|
||||
selectedDocsState,
|
||||
paginationObj,
|
||||
valueToStringConverter,
|
||||
]
|
||||
);
|
||||
|
||||
const shouldShowFieldHandler = useMemo(() => {
|
||||
const dataViewFields = dataView.fields.getAll().map((fld) => fld.name);
|
||||
return getShouldShowFieldHandler(dataViewFields, dataView, showMultiFields);
|
||||
|
@ -885,44 +873,41 @@ export const UnifiedDataTable = ({
|
|||
controlColumnsConfig?.trailingControlColumns ?? trailingControlColumns;
|
||||
|
||||
const additionalControls = useMemo(() => {
|
||||
if (!externalAdditionalControls && !usedSelectedDocs.length) {
|
||||
if (!externalAdditionalControls && !selectedDocIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Boolean(usedSelectedDocs.length) && (
|
||||
<EuiFlexGroup gutterSize="s" responsive={false}>
|
||||
{enableComparisonMode && usedSelectedDocs.length > 1 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<DataTableCompareToolbarBtn
|
||||
selectedDocs={usedSelectedDocs}
|
||||
setIsCompareActive={setIsCompareActive}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DataTableDocumentToolbarBtn
|
||||
isPlainRecord={isPlainRecord}
|
||||
isFilterActive={isFilterActive}
|
||||
rows={rows!}
|
||||
selectedDocs={usedSelectedDocs}
|
||||
setSelectedDocs={setSelectedDocs}
|
||||
setIsFilterActive={setIsFilterActive}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{Boolean(selectedDocIds.length) && (
|
||||
<DataTableDocumentToolbarBtn
|
||||
isPlainRecord={isPlainRecord}
|
||||
isFilterActive={isFilterActive}
|
||||
rows={rows!}
|
||||
setIsFilterActive={setIsFilterActive}
|
||||
selectedDocsState={selectedDocsState}
|
||||
enableComparisonMode={enableComparisonMode}
|
||||
setIsCompareActive={setIsCompareActive}
|
||||
fieldFormats={fieldFormats}
|
||||
pageIndex={unifiedDataTableContextValue.pageIndex}
|
||||
pageSize={unifiedDataTableContextValue.pageSize}
|
||||
/>
|
||||
)}
|
||||
{externalAdditionalControls}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
selectedDocIds,
|
||||
selectedDocsState,
|
||||
externalAdditionalControls,
|
||||
usedSelectedDocs,
|
||||
isPlainRecord,
|
||||
isFilterActive,
|
||||
setIsFilterActive,
|
||||
enableComparisonMode,
|
||||
rows,
|
||||
fieldFormats,
|
||||
unifiedDataTableContextValue.pageIndex,
|
||||
unifiedDataTableContextValue.pageSize,
|
||||
]);
|
||||
|
||||
const renderCustomToolbarFn: EuiDataGridProps['renderCustomToolbar'] | undefined = useMemo(
|
||||
|
@ -1079,13 +1064,13 @@ export const UnifiedDataTable = ({
|
|||
isPlainRecord={isPlainRecord}
|
||||
selectedFieldNames={visibleColumns}
|
||||
additionalFieldGroups={additionalFieldGroups}
|
||||
selectedDocs={selectedDocs}
|
||||
selectedDocIds={selectedDocIds}
|
||||
schemaDetectors={schemaDetectors}
|
||||
forceShowAllFields={defaultColumns}
|
||||
showFullScreenButton={showFullScreenButton}
|
||||
fieldFormats={fieldFormats}
|
||||
getDocById={getDocById}
|
||||
setSelectedDocs={setSelectedDocs}
|
||||
replaceSelectedDocs={replaceSelectedDocs}
|
||||
setIsCompareActive={setIsCompareActive}
|
||||
/>
|
||||
) : (
|
||||
|
@ -1117,9 +1102,9 @@ export const UnifiedDataTable = ({
|
|||
)}
|
||||
</div>
|
||||
{loadingState !== DataLoadingState.loading &&
|
||||
!usedSelectedDocs.length && // hide footer when showing selected documents
|
||||
isPaginationEnabled &&
|
||||
!isCompareActive && ( // we hide the footer for Surrounding Documents page
|
||||
isPaginationEnabled && // we hide the footer for Surrounding Documents page
|
||||
!isFilterActive && // hide footer when showing selected documents
|
||||
!isCompareActive && (
|
||||
<UnifiedDataTableFooter
|
||||
isLoadingMore={loadingState === DataLoadingState.loadingMore}
|
||||
rowCount={rowCount}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { ControlColumns, CustomGridColumnsConfiguration, UnifiedDataTableSetting
|
|||
import type { ValueToStringConverter, DataTableColumnsMeta } from '../types';
|
||||
import { buildCellActions } from './default_cell_actions';
|
||||
import { getSchemaByKbnType } from './data_table_schema';
|
||||
import { SelectButton } from './data_table_document_selection';
|
||||
import { SelectButton, SelectAllButton } from './data_table_document_selection';
|
||||
import { defaultTimeColumnWidth, ROWS_HEIGHT_OPTIONS } from '../constants';
|
||||
import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_copy_column_button';
|
||||
import { buildEditFieldButton } from './build_edit_field_button';
|
||||
|
@ -70,15 +70,7 @@ const select = {
|
|||
id: SELECT_ROW,
|
||||
width: 24,
|
||||
rowCellRender: SelectButton,
|
||||
headerCellRender: () => (
|
||||
<EuiScreenReaderOnly>
|
||||
<span>
|
||||
{i18n.translate('unifiedDataTable.selectColumnHeader', {
|
||||
defaultMessage: 'Select column',
|
||||
})}
|
||||
</span>
|
||||
</EuiScreenReaderOnly>
|
||||
),
|
||||
headerCellRender: SelectAllButton,
|
||||
};
|
||||
|
||||
export function getAllControlColumns(): ControlColumns {
|
||||
|
|
|
@ -6,18 +6,21 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import {
|
||||
DataTableCompareToolbarBtn,
|
||||
DataTableDocumentToolbarBtn,
|
||||
SelectButton,
|
||||
SelectAllButton,
|
||||
} from './data_table_document_selection';
|
||||
import { dataTableContextMock } from '../../__mocks__/table_context';
|
||||
import { buildSelectedDocsState, dataTableContextMock } from '../../__mocks__/table_context';
|
||||
import { UnifiedDataTableContext } from '../table_context';
|
||||
import { getDocId } from '@kbn/discover-utils';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { servicesMock } from '../../__mocks__/services';
|
||||
|
||||
describe('document selection', () => {
|
||||
describe('getDocId', () => {
|
||||
|
@ -39,6 +42,39 @@ describe('document selection', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('SelectAllButton', () => {
|
||||
test('is not checked', () => {
|
||||
const contextMock = {
|
||||
...dataTableContextMock,
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<UnifiedDataTableContext.Provider value={contextMock}>
|
||||
<SelectAllButton />
|
||||
</UnifiedDataTableContext.Provider>
|
||||
);
|
||||
|
||||
const checkBox = findTestSubject(component, 'selectAllDocsOnPageToggle');
|
||||
expect(checkBox.props().checked).toBeFalsy();
|
||||
});
|
||||
|
||||
test('is checked correctly', () => {
|
||||
const contextMock = {
|
||||
...dataTableContextMock,
|
||||
selectedDocsState: buildSelectedDocsState(['i::1::']),
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<UnifiedDataTableContext.Provider value={contextMock}>
|
||||
<SelectAllButton />
|
||||
</UnifiedDataTableContext.Provider>
|
||||
);
|
||||
|
||||
const checkBox = findTestSubject(component, 'selectAllDocsOnPageToggle');
|
||||
expect(checkBox.props().checked).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SelectButton', () => {
|
||||
test('is not checked', () => {
|
||||
const contextMock = {
|
||||
|
@ -63,13 +99,13 @@ describe('document selection', () => {
|
|||
expect(checkBox.props().checked).toBeFalsy();
|
||||
});
|
||||
|
||||
test('is checked', () => {
|
||||
test('is checked correctly', () => {
|
||||
const contextMock = {
|
||||
...dataTableContextMock,
|
||||
selectedDocs: ['i::1::'],
|
||||
selectedDocsState: buildSelectedDocsState(['i::1::']),
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
const component1 = mountWithIntl(
|
||||
<UnifiedDataTableContext.Provider value={contextMock}>
|
||||
<SelectButton
|
||||
rowIndex={0}
|
||||
|
@ -83,8 +119,25 @@ describe('document selection', () => {
|
|||
</UnifiedDataTableContext.Provider>
|
||||
);
|
||||
|
||||
const checkBox = findTestSubject(component, 'dscGridSelectDoc-i::1::');
|
||||
expect(checkBox.props().checked).toBeTruthy();
|
||||
const checkBox1 = findTestSubject(component1, 'dscGridSelectDoc-i::1::');
|
||||
expect(checkBox1.props().checked).toBeTruthy();
|
||||
|
||||
const component2 = mountWithIntl(
|
||||
<UnifiedDataTableContext.Provider value={contextMock}>
|
||||
<SelectButton
|
||||
rowIndex={1}
|
||||
colIndex={0}
|
||||
setCellProps={jest.fn()}
|
||||
columnId="test"
|
||||
isExpanded={false}
|
||||
isDetails={false}
|
||||
isExpandable={false}
|
||||
/>
|
||||
</UnifiedDataTableContext.Provider>
|
||||
);
|
||||
|
||||
const checkBox2 = findTestSubject(component2, 'dscGridSelectDoc-i::2::');
|
||||
expect(checkBox2.props().checked).toBeFalsy();
|
||||
});
|
||||
|
||||
test('adding a selection', () => {
|
||||
|
@ -108,13 +161,13 @@ describe('document selection', () => {
|
|||
|
||||
const checkBox = findTestSubject(component, 'dscGridSelectDoc-i::1::');
|
||||
checkBox.simulate('change');
|
||||
expect(contextMock.setSelectedDocs).toHaveBeenCalledWith(['i::1::']);
|
||||
expect(contextMock.selectedDocsState.toggleDocSelection).toHaveBeenCalledWith('i::1::');
|
||||
});
|
||||
|
||||
test('removing a selection', () => {
|
||||
const contextMock = {
|
||||
...dataTableContextMock,
|
||||
selectedDocs: ['i::1::'],
|
||||
selectedDocsState: buildSelectedDocsState(['i::1::']),
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
|
@ -133,55 +186,192 @@ describe('document selection', () => {
|
|||
|
||||
const checkBox = findTestSubject(component, 'dscGridSelectDoc-i::1::');
|
||||
checkBox.simulate('change');
|
||||
expect(contextMock.setSelectedDocs).toHaveBeenCalledWith([]);
|
||||
expect(contextMock.selectedDocsState.toggleDocSelection).toHaveBeenCalledWith('i::1::');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DataTableDocumentToolbarBtn', () => {
|
||||
test('it renders a button clickable button', () => {
|
||||
test('it renders the button and its menu correctly', () => {
|
||||
const props = {
|
||||
isPlainRecord: false,
|
||||
isFilterActive: false,
|
||||
rows: dataTableContextMock.rows,
|
||||
selectedDocs: ['i::1::'],
|
||||
selectedDocsState: buildSelectedDocsState(['i::1::', 'i::2::']),
|
||||
setIsFilterActive: jest.fn(),
|
||||
setSelectedDocs: jest.fn(),
|
||||
enableComparisonMode: true,
|
||||
setIsCompareActive: jest.fn(),
|
||||
fieldFormats: servicesMock.fieldFormats,
|
||||
pageIndex: 0,
|
||||
pageSize: 2,
|
||||
};
|
||||
const component = mountWithIntl(<DataTableDocumentToolbarBtn {...props} />);
|
||||
const button = findTestSubject(component, 'unifiedDataTableSelectionBtn');
|
||||
expect(button.length).toBe(1);
|
||||
expect(button.text()).toBe('Selected2');
|
||||
|
||||
act(() => {
|
||||
button.simulate('click');
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
expect(findTestSubject(component, 'dscGridShowSelectedDocuments').length).toBe(1);
|
||||
expect(findTestSubject(component, 'unifiedDataTableCompareSelectedDocuments').length).toBe(1);
|
||||
expect(findTestSubject(component, 'dscGridSelectAllDocs').text()).toBe('Select all 5');
|
||||
|
||||
act(() => {
|
||||
findTestSubject(component, 'dscGridClearSelectedDocuments').simulate('click');
|
||||
});
|
||||
|
||||
expect(props.selectedDocsState.clearAllSelectedDocs).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('it should not render "Select all X" button if less than pageSize is selected', () => {
|
||||
const props = {
|
||||
isPlainRecord: false,
|
||||
isFilterActive: false,
|
||||
rows: dataTableContextMock.rows,
|
||||
selectedDocsState: buildSelectedDocsState(['i::1::']),
|
||||
setIsFilterActive: jest.fn(),
|
||||
enableComparisonMode: true,
|
||||
setIsCompareActive: jest.fn(),
|
||||
fieldFormats: servicesMock.fieldFormats,
|
||||
pageIndex: 0,
|
||||
pageSize: 2,
|
||||
};
|
||||
const component = mountWithIntl(<DataTableDocumentToolbarBtn {...props} />);
|
||||
expect(findTestSubject(component, 'unifiedDataTableSelectionBtn').text()).toBe('Selected1');
|
||||
|
||||
expect(findTestSubject(component, 'dscGridSelectAllDocs').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it should render "Select all X" button if all rows on the page are selected', () => {
|
||||
const props = {
|
||||
isPlainRecord: false,
|
||||
isFilterActive: false,
|
||||
rows: dataTableContextMock.rows,
|
||||
selectedDocsState: buildSelectedDocsState(['i::1::', 'i::2::']),
|
||||
setIsFilterActive: jest.fn(),
|
||||
enableComparisonMode: true,
|
||||
setIsCompareActive: jest.fn(),
|
||||
fieldFormats: servicesMock.fieldFormats,
|
||||
pageIndex: 0,
|
||||
pageSize: 2,
|
||||
};
|
||||
const component = mountWithIntl(<DataTableDocumentToolbarBtn {...props} />);
|
||||
expect(findTestSubject(component, 'unifiedDataTableSelectionBtn').text()).toBe('Selected2');
|
||||
|
||||
const button = findTestSubject(component, 'dscGridSelectAllDocs');
|
||||
expect(button.exists()).toBe(true);
|
||||
|
||||
act(() => {
|
||||
button.simulate('click');
|
||||
});
|
||||
|
||||
expect(props.selectedDocsState.selectAllDocs).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('it should render "Select all X" button even if on another page', () => {
|
||||
const props = {
|
||||
isPlainRecord: false,
|
||||
isFilterActive: false,
|
||||
rows: dataTableContextMock.rows,
|
||||
selectedDocsState: buildSelectedDocsState(['i::1::', 'i::2::']),
|
||||
setIsFilterActive: jest.fn(),
|
||||
enableComparisonMode: true,
|
||||
setIsCompareActive: jest.fn(),
|
||||
fieldFormats: servicesMock.fieldFormats,
|
||||
pageIndex: 1,
|
||||
pageSize: 2,
|
||||
};
|
||||
const component = mountWithIntl(<DataTableDocumentToolbarBtn {...props} />);
|
||||
expect(findTestSubject(component, 'unifiedDataTableSelectionBtn').text()).toBe('Selected2');
|
||||
|
||||
expect(findTestSubject(component, 'dscGridSelectAllDocs').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it should not render "Select all X" button if all rows are selected', () => {
|
||||
const props = {
|
||||
isPlainRecord: false,
|
||||
isFilterActive: false,
|
||||
rows: dataTableContextMock.rows,
|
||||
selectedDocsState: buildSelectedDocsState(dataTableContextMock.rows.map((row) => row.id)),
|
||||
setIsFilterActive: jest.fn(),
|
||||
enableComparisonMode: true,
|
||||
setIsCompareActive: jest.fn(),
|
||||
fieldFormats: servicesMock.fieldFormats,
|
||||
pageIndex: 1,
|
||||
pageSize: 2,
|
||||
};
|
||||
const component = mountWithIntl(<DataTableDocumentToolbarBtn {...props} />);
|
||||
expect(findTestSubject(component, 'unifiedDataTableSelectionBtn').text()).toBe(
|
||||
`Selected${dataTableContextMock.rows.length}`
|
||||
);
|
||||
|
||||
expect(findTestSubject(component, 'dscGridSelectAllDocs').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DataTableCompareToolbarBtn', () => {
|
||||
const props = {
|
||||
isPlainRecord: false,
|
||||
isFilterActive: false,
|
||||
rows: dataTableContextMock.rows,
|
||||
selectedDocsState: buildSelectedDocsState([]),
|
||||
setIsFilterActive: jest.fn(),
|
||||
enableComparisonMode: true,
|
||||
setIsCompareActive: jest.fn(),
|
||||
fieldFormats: servicesMock.fieldFormats,
|
||||
pageIndex: 0,
|
||||
pageSize: 2,
|
||||
};
|
||||
|
||||
const renderCompareBtn = ({
|
||||
selectedDocs = ['1', '2'],
|
||||
selectedDocIds = ['1', '2'],
|
||||
setIsCompareActive = jest.fn(),
|
||||
}: Partial<Parameters<typeof DataTableCompareToolbarBtn>[0]> = {}) => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<DataTableCompareToolbarBtn
|
||||
selectedDocs={selectedDocs}
|
||||
<DataTableDocumentToolbarBtn
|
||||
{...props}
|
||||
selectedDocsState={buildSelectedDocsState(selectedDocIds)}
|
||||
setIsCompareActive={setIsCompareActive}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
return {
|
||||
getButton: () => screen.queryByRole('button', { name: /Compare/ }),
|
||||
getButton: async () => {
|
||||
const menuButton = await screen.findByTestId('unifiedDataTableSelectionBtn');
|
||||
menuButton.click();
|
||||
return screen.queryByRole('button', { name: /Compare/ });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it('should render the compare button', () => {
|
||||
it('should render the compare button', async () => {
|
||||
const { getButton } = renderCompareBtn();
|
||||
expect(getButton()).toBeInTheDocument();
|
||||
expect(await getButton()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call setIsCompareActive when the button is clicked', () => {
|
||||
it('should call setIsCompareActive when the button is clicked', async () => {
|
||||
const setIsCompareActive = jest.fn();
|
||||
const { getButton } = renderCompareBtn({ setIsCompareActive });
|
||||
getButton()?.click();
|
||||
const button = await getButton();
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button?.getAttribute('disabled')).toBeNull();
|
||||
button?.click();
|
||||
expect(setIsCompareActive).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should disable the button if limit is reached', async () => {
|
||||
const selectedDocIds = Array.from({ length: 500 }, (_, i) => i.toString());
|
||||
const setIsCompareActive = jest.fn();
|
||||
const { getButton } = renderCompareBtn({ selectedDocIds, setIsCompareActive });
|
||||
const button = await getButton();
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button?.getAttribute('disabled')).toBe('');
|
||||
button?.click();
|
||||
expect(setIsCompareActive).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
import {
|
||||
EuiCheckbox,
|
||||
EuiContextMenuItem,
|
||||
|
@ -16,20 +18,22 @@ import {
|
|||
EuiPopover,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
EuiScreenReaderOnly,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import type { UseSelectedDocsState } from '../hooks/use_selected_docs';
|
||||
import { UnifiedDataTableContext } from '../table_context';
|
||||
|
||||
export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { selectedDocs, expanded, rows, isDarkMode, setSelectedDocs } =
|
||||
useContext(UnifiedDataTableContext);
|
||||
const { selectedDocsState, expanded, rows, isDarkMode } = useContext(UnifiedDataTableContext);
|
||||
const { isDocSelected, toggleDocSelection } = selectedDocsState;
|
||||
const doc = useMemo(() => rows[rowIndex], [rows, rowIndex]);
|
||||
const checked = useMemo(() => selectedDocs.includes(doc.id), [selectedDocs, doc.id]);
|
||||
|
||||
const toggleDocumentSelectionLabel = i18n.translate('unifiedDataTable.grid.selectDoc', {
|
||||
defaultMessage: `Select document ''{rowNumber}''`,
|
||||
|
@ -42,7 +46,7 @@ export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
|
|||
className: 'unifiedDataTable__cell--selected',
|
||||
});
|
||||
} else {
|
||||
setCellProps({ style: undefined });
|
||||
setCellProps({ className: '' });
|
||||
}
|
||||
}, [expanded, doc, setCellProps, isDarkMode]);
|
||||
|
||||
|
@ -61,15 +65,10 @@ export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
|
|||
<EuiCheckbox
|
||||
id={doc.id}
|
||||
aria-label={toggleDocumentSelectionLabel}
|
||||
checked={checked}
|
||||
checked={isDocSelected(doc.id)}
|
||||
data-test-subj={`dscGridSelectDoc-${doc.id}`}
|
||||
onChange={() => {
|
||||
if (checked) {
|
||||
const newSelection = selectedDocs.filter((docId) => docId !== doc.id);
|
||||
setSelectedDocs(newSelection);
|
||||
} else {
|
||||
setSelectedDocs([...selectedDocs, doc.id]);
|
||||
}
|
||||
toggleDocSelection(doc.id);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -77,26 +76,149 @@ export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
|
|||
);
|
||||
};
|
||||
|
||||
export const SelectAllButton = () => {
|
||||
const { selectedDocsState, pageIndex, pageSize, rows } = useContext(UnifiedDataTableContext);
|
||||
const { getCountOfSelectedDocs, deselectSomeDocs, selectMoreDocs } = selectedDocsState;
|
||||
|
||||
const docIdsFromCurrentPage = useMemo(() => {
|
||||
return getDocIdsForCurrentPage(rows, pageIndex, pageSize);
|
||||
}, [rows, pageIndex, pageSize]);
|
||||
|
||||
const countOfSelectedDocs = useMemo(() => {
|
||||
return docIdsFromCurrentPage?.length ? getCountOfSelectedDocs(docIdsFromCurrentPage) : 0;
|
||||
}, [docIdsFromCurrentPage, getCountOfSelectedDocs]);
|
||||
|
||||
const isIndeterminateForCurrentPage = useMemo(() => {
|
||||
if (docIdsFromCurrentPage?.length) {
|
||||
return countOfSelectedDocs > 0 && countOfSelectedDocs < docIdsFromCurrentPage.length;
|
||||
}
|
||||
return false;
|
||||
}, [docIdsFromCurrentPage, countOfSelectedDocs]);
|
||||
|
||||
const areDocsSelectedForCurrentPage = useMemo(() => {
|
||||
if (docIdsFromCurrentPage?.length) {
|
||||
return countOfSelectedDocs > 0;
|
||||
}
|
||||
return false;
|
||||
}, [docIdsFromCurrentPage, countOfSelectedDocs]);
|
||||
|
||||
if (!docIdsFromCurrentPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title =
|
||||
isIndeterminateForCurrentPage || areDocsSelectedForCurrentPage
|
||||
? i18n.translate('unifiedDataTable.deselectAllRowsOnPageColumnHeader', {
|
||||
defaultMessage: 'Deselect all visible rows',
|
||||
})
|
||||
: i18n.translate('unifiedDataTable.selectAllRowsOnPageColumnHeader', {
|
||||
defaultMessage: 'Select all visible rows',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiScreenReaderOnly>
|
||||
<span>
|
||||
{i18n.translate('unifiedDataTable.selectColumnHeader', {
|
||||
defaultMessage: 'Select column',
|
||||
})}
|
||||
</span>
|
||||
</EuiScreenReaderOnly>
|
||||
<EuiCheckbox
|
||||
data-test-subj="selectAllDocsOnPageToggle"
|
||||
id="select-all-docs-on-page-toggle"
|
||||
aria-label={title}
|
||||
title={title}
|
||||
indeterminate={isIndeterminateForCurrentPage}
|
||||
checked={areDocsSelectedForCurrentPage}
|
||||
onChange={(e) => {
|
||||
const shouldClearSelection = isIndeterminateForCurrentPage || !e.target.checked;
|
||||
|
||||
if (shouldClearSelection) {
|
||||
deselectSomeDocs(docIdsFromCurrentPage);
|
||||
} else {
|
||||
selectMoreDocs(docIdsFromCurrentPage);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function DataTableDocumentToolbarBtn({
|
||||
isPlainRecord,
|
||||
isFilterActive,
|
||||
rows,
|
||||
selectedDocs,
|
||||
setIsFilterActive,
|
||||
setSelectedDocs,
|
||||
selectedDocsState,
|
||||
enableComparisonMode,
|
||||
setIsCompareActive,
|
||||
fieldFormats,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}: {
|
||||
isPlainRecord: boolean;
|
||||
isFilterActive: boolean;
|
||||
rows: DataTableRecord[];
|
||||
selectedDocs: string[];
|
||||
setIsFilterActive: (value: boolean) => void;
|
||||
setSelectedDocs: (value: string[]) => void;
|
||||
selectedDocsState: UseSelectedDocsState;
|
||||
enableComparisonMode: boolean | undefined;
|
||||
setIsCompareActive: (value: boolean) => void;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
pageIndex: number | undefined;
|
||||
pageSize: number | undefined;
|
||||
}) {
|
||||
const [isSelectionPopoverOpen, setIsSelectionPopoverOpen] = useState(false);
|
||||
const { selectAllDocs, clearAllSelectedDocs, isDocSelected, selectedDocIds } = selectedDocsState;
|
||||
|
||||
const shouldSuggestToSelectAll = useMemo(() => {
|
||||
const canSelectMore = selectedDocIds.length < rows.length && rows.length > 1;
|
||||
if (typeof pageSize !== 'number' || isFilterActive || !canSelectMore) {
|
||||
return false;
|
||||
}
|
||||
return selectedDocIds.length >= pageSize;
|
||||
}, [rows, pageSize, selectedDocIds.length, isFilterActive]);
|
||||
|
||||
const getMenuItems = useCallback(() => {
|
||||
return [
|
||||
// Compare selected documents
|
||||
...(enableComparisonMode && selectedDocIds.length > 1
|
||||
? [
|
||||
<DataTableCompareToolbarBtn
|
||||
key="compareSelected"
|
||||
selectedDocIds={selectedDocIds}
|
||||
setIsCompareActive={setIsCompareActive}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
// Copy results to clipboard (JSON)
|
||||
<EuiCopy
|
||||
key="copyJsonWrapper"
|
||||
data-test-subj="dscGridCopySelectedDocumentsJSON"
|
||||
textToCopy={
|
||||
rows
|
||||
? JSON.stringify(rows.filter((row) => isDocSelected(row.id)).map((row) => row.raw))
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{(copy) => (
|
||||
<EuiContextMenuItem key="copyJSON" icon="copyClipboard" onClick={copy}>
|
||||
{isPlainRecord ? (
|
||||
<FormattedMessage
|
||||
id="unifiedDataTable.copyResultsToClipboardJSON"
|
||||
defaultMessage="Copy results to clipboard (JSON)"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="unifiedDataTable.copyToClipboardJSON"
|
||||
defaultMessage="Copy documents to clipboard (JSON)"
|
||||
/>
|
||||
)}
|
||||
</EuiContextMenuItem>
|
||||
)}
|
||||
</EuiCopy>,
|
||||
isFilterActive ? (
|
||||
// Show all documents
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="dscGridShowAllDocuments"
|
||||
key="showAllDocuments"
|
||||
|
@ -119,6 +241,7 @@ export function DataTableDocumentToolbarBtn({
|
|||
)}
|
||||
</EuiContextMenuItem>
|
||||
) : (
|
||||
// Show selected documents only
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="dscGridShowSelectedDocuments"
|
||||
key="showSelectedDocuments"
|
||||
|
@ -141,66 +264,58 @@ export function DataTableDocumentToolbarBtn({
|
|||
)}
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
<EuiCopy
|
||||
key="copyJsonWrapper"
|
||||
data-test-subj="dscGridCopySelectedDocumentsJSON"
|
||||
textToCopy={
|
||||
rows
|
||||
? JSON.stringify(
|
||||
rows.filter((row) => selectedDocs.includes(row.id)).map((row) => row.raw)
|
||||
)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{(copy) => (
|
||||
<EuiContextMenuItem key="copyJSON" icon="copyClipboard" onClick={copy}>
|
||||
{isPlainRecord ? (
|
||||
<FormattedMessage
|
||||
id="unifiedDataTable.copyResultsToClipboardJSON"
|
||||
defaultMessage="Copy results to clipboard (JSON)"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="unifiedDataTable.copyToClipboardJSON"
|
||||
defaultMessage="Copy documents to clipboard (JSON)"
|
||||
/>
|
||||
)}
|
||||
</EuiContextMenuItem>
|
||||
)}
|
||||
</EuiCopy>,
|
||||
// Clear selection
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="dscGridClearSelectedDocuments"
|
||||
key="clearSelection"
|
||||
icon="cross"
|
||||
onClick={() => {
|
||||
setIsSelectionPopoverOpen(false);
|
||||
setSelectedDocs([]);
|
||||
clearAllSelectedDocs();
|
||||
setIsFilterActive(false);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="unifiedDataTable.clearSelection" defaultMessage="Clear selection" />
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
}, [isFilterActive, isPlainRecord, rows, selectedDocs, setIsFilterActive, setSelectedDocs]);
|
||||
}, [
|
||||
isFilterActive,
|
||||
isPlainRecord,
|
||||
rows,
|
||||
setIsFilterActive,
|
||||
isDocSelected,
|
||||
clearAllSelectedDocs,
|
||||
selectedDocIds,
|
||||
enableComparisonMode,
|
||||
setIsCompareActive,
|
||||
]);
|
||||
|
||||
const toggleSelectionToolbar = useCallback(
|
||||
() => setIsSelectionPopoverOpen((prevIsOpen) => !prevIsOpen),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
const selectedRowsMenuButton = (
|
||||
<EuiPopover
|
||||
closePopover={() => setIsSelectionPopoverOpen(false)}
|
||||
isOpen={isSelectionPopoverOpen}
|
||||
panelPaddingSize="none"
|
||||
button={
|
||||
<EuiDataGridToolbarControl
|
||||
iconType="documents"
|
||||
iconSide="left"
|
||||
iconType="arrowDown"
|
||||
onClick={toggleSelectionToolbar}
|
||||
data-selected-documents={selectedDocs.length}
|
||||
data-selected-documents={selectedDocIds.length}
|
||||
data-test-subj="unifiedDataTableSelectionBtn"
|
||||
isSelected={isFilterActive}
|
||||
badgeContent={selectedDocs.length}
|
||||
badgeContent={fieldFormats
|
||||
.getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER])
|
||||
.convert(selectedDocIds.length)}
|
||||
css={css`
|
||||
.euiButtonEmpty__content {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{isPlainRecord ? (
|
||||
<FormattedMessage
|
||||
|
@ -226,28 +341,92 @@ export function DataTableDocumentToolbarBtn({
|
|||
)}
|
||||
</EuiPopover>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
gutterSize="none"
|
||||
wrap={false}
|
||||
className="unifiedDataTableToolbarControlGroup"
|
||||
>
|
||||
<EuiFlexItem className="unifiedDataTableToolbarControlButton" grow={false}>
|
||||
{selectedRowsMenuButton}
|
||||
</EuiFlexItem>
|
||||
{shouldSuggestToSelectAll ? (
|
||||
<EuiFlexItem className="unifiedDataTableToolbarControlButton" grow={false}>
|
||||
<EuiDataGridToolbarControl
|
||||
data-test-subj="dscGridSelectAllDocs"
|
||||
onClick={() => {
|
||||
setIsSelectionPopoverOpen(false);
|
||||
selectAllDocs();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="unifiedDataTable.selectAllDocs"
|
||||
defaultMessage="Select all {rowsCount}"
|
||||
values={{
|
||||
rowsCount: fieldFormats
|
||||
.getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER])
|
||||
.convert(rows.length),
|
||||
}}
|
||||
/>
|
||||
</EuiDataGridToolbarControl>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_SELECTED_DOCS_FOR_COMPARE = 100;
|
||||
|
||||
export const DataTableCompareToolbarBtn = ({
|
||||
selectedDocs,
|
||||
selectedDocIds,
|
||||
setIsCompareActive,
|
||||
}: {
|
||||
selectedDocs: string[];
|
||||
selectedDocIds: string[];
|
||||
setIsCompareActive: (value: boolean) => void;
|
||||
}) => {
|
||||
const isDisabled = selectedDocIds.length > MAX_SELECTED_DOCS_FOR_COMPARE;
|
||||
const label = (
|
||||
<FormattedMessage
|
||||
id="unifiedDataTable.compareSelectedRowsButtonLabel"
|
||||
defaultMessage="Compare selected"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<EuiDataGridToolbarControl
|
||||
iconType="diff"
|
||||
badgeContent={selectedDocs.length}
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="unifiedDataTableCompareSelectedDocuments"
|
||||
disabled={isDisabled}
|
||||
icon="diff"
|
||||
onClick={() => {
|
||||
setIsCompareActive(true);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="unifiedDataTable.compareSelectedRowsButtonLabel"
|
||||
defaultMessage="Compare"
|
||||
/>
|
||||
</EuiDataGridToolbarControl>
|
||||
{isDisabled ? (
|
||||
<EuiToolTip
|
||||
content={i18n.translate('unifiedDataTable.compareSelectedRowsButtonDisabledTooltip', {
|
||||
defaultMessage: 'Comparison is limited to {limit} rows',
|
||||
values: { limit: MAX_SELECTED_DOCS_FOR_COMPARE },
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
function getDocIdsForCurrentPage(
|
||||
rows: DataTableRecord[],
|
||||
pageIndex: number | undefined,
|
||||
pageSize: number | undefined
|
||||
): string[] | undefined {
|
||||
if (typeof pageIndex === 'number' && typeof pageSize === 'number') {
|
||||
const start = pageIndex * pageSize;
|
||||
const end = start + pageSize;
|
||||
return rows.slice(start, end).map((row) => row.id);
|
||||
}
|
||||
return undefined; // pagination is disabled
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
|
|||
className: 'unifiedDataTable__cell--expanded',
|
||||
});
|
||||
} else {
|
||||
setCellProps({ style: undefined });
|
||||
setCellProps({ className: '' });
|
||||
}
|
||||
}, [expanded, current, setCellProps, isDarkMode]);
|
||||
|
||||
|
|
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import { useSelectedDocs } from './use_selected_docs';
|
||||
import { generateEsHits } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
|
||||
|
||||
describe('useSelectedDocs', () => {
|
||||
const docs = generateEsHits(dataViewWithTimefieldMock, 5).map((hit) =>
|
||||
buildDataTableRecord(hit, dataViewWithTimefieldMock)
|
||||
);
|
||||
const docsMap = new Map(docs.map((doc) => [doc.id, doc]));
|
||||
|
||||
test('should have a correct default state', () => {
|
||||
const { result } = renderHook(() => useSelectedDocs(docsMap));
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [],
|
||||
hasSelectedDocs: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should toggleDocSelection correctly', () => {
|
||||
const { result } = renderHook(() => useSelectedDocs(docsMap));
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[0].id);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[0].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.isDocSelected(docs[0].id)).toBe(true);
|
||||
expect(result.current.isDocSelected(docs[1].id)).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[1].id);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[0].id, docs[1].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.isDocSelected(docs[0].id)).toBe(true);
|
||||
expect(result.current.isDocSelected(docs[1].id)).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[0].id);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[1].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.isDocSelected(docs[0].id)).toBe(false);
|
||||
expect(result.current.isDocSelected(docs[1].id)).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[1].id);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [],
|
||||
hasSelectedDocs: false,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.isDocSelected(docs[0].id)).toBe(false);
|
||||
expect(result.current.isDocSelected(docs[1].id)).toBe(false);
|
||||
});
|
||||
|
||||
test('should replaceSelectedDocs correctly', () => {
|
||||
const { result } = renderHook(() => useSelectedDocs(docsMap));
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[0].id);
|
||||
result.current.toggleDocSelection(docs[1].id);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[0].id, docs[1].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.replaceSelectedDocs([docs[1].id, docs[2].id]);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[1].id, docs[2].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.isDocSelected(docs[0].id)).toBe(false);
|
||||
expect(result.current.isDocSelected(docs[1].id)).toBe(true);
|
||||
expect(result.current.isDocSelected(docs[2].id)).toBe(true);
|
||||
});
|
||||
|
||||
test('should selectAllDocs correctly', () => {
|
||||
const { result } = renderHook(() => useSelectedDocs(docsMap));
|
||||
|
||||
act(() => {
|
||||
result.current.selectAllDocs();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: docs.map((doc) => doc.id),
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.isDocSelected(docs[0].id)).toBe(true);
|
||||
expect(result.current.isDocSelected(docs[docs.length - 1].id)).toBe(true);
|
||||
});
|
||||
|
||||
test('should selectMoreDocs correctly', () => {
|
||||
const { result } = renderHook(() => useSelectedDocs(docsMap));
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[0].id);
|
||||
result.current.toggleDocSelection(docs[1].id);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[0].id, docs[1].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.selectMoreDocs([docs[1].id, docs[2].id]);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[0].id, docs[1].id, docs[2].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.isDocSelected(docs[0].id)).toBe(true);
|
||||
expect(result.current.isDocSelected(docs[1].id)).toBe(true);
|
||||
expect(result.current.isDocSelected(docs[2].id)).toBe(true);
|
||||
});
|
||||
|
||||
test('should deselectSomeDocs correctly', () => {
|
||||
const { result } = renderHook(() => useSelectedDocs(docsMap));
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[0].id);
|
||||
result.current.toggleDocSelection(docs[1].id);
|
||||
result.current.toggleDocSelection(docs[2].id);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[0].id, docs[1].id, docs[2].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.deselectSomeDocs([docs[0].id, docs[2].id]);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[1].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.isDocSelected(docs[0].id)).toBe(false);
|
||||
expect(result.current.isDocSelected(docs[1].id)).toBe(true);
|
||||
expect(result.current.isDocSelected(docs[2].id)).toBe(false);
|
||||
});
|
||||
|
||||
test('should clearAllSelectedDocs correctly', () => {
|
||||
const { result } = renderHook(() => useSelectedDocs(docsMap));
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[0].id);
|
||||
result.current.toggleDocSelection(docs[1].id);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [docs[0].id, docs[1].id],
|
||||
hasSelectedDocs: true,
|
||||
})
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.clearAllSelectedDocs();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
selectedDocIds: [],
|
||||
hasSelectedDocs: false,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.isDocSelected(docs[0].id)).toBe(false);
|
||||
expect(result.current.isDocSelected(docs[1].id)).toBe(false);
|
||||
});
|
||||
|
||||
test('should getCountOfSelectedDocs correctly', () => {
|
||||
const { result } = renderHook(() => useSelectedDocs(docsMap));
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[0].id);
|
||||
result.current.toggleDocSelection(docs[1].id);
|
||||
});
|
||||
|
||||
expect(result.current.getCountOfSelectedDocs([docs[0].id, docs[1].id])).toBe(2);
|
||||
expect(result.current.getCountOfSelectedDocs([docs[2].id, docs[3].id])).toBe(0);
|
||||
|
||||
act(() => {
|
||||
result.current.toggleDocSelection(docs[0].id);
|
||||
});
|
||||
|
||||
expect(result.current.getCountOfSelectedDocs([docs[0].id, docs[1].id])).toBe(1);
|
||||
expect(result.current.getCountOfSelectedDocs([docs[1].id])).toBe(1);
|
||||
expect(result.current.getCountOfSelectedDocs([docs[0].id])).toBe(0);
|
||||
expect(result.current.getCountOfSelectedDocs([docs[2].id, docs[3].id])).toBe(0);
|
||||
});
|
||||
});
|
112
packages/kbn-unified-data-table/src/hooks/use_selected_docs.ts
Normal file
112
packages/kbn-unified-data-table/src/hooks/use_selected_docs.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils';
|
||||
|
||||
export interface UseSelectedDocsState {
|
||||
isDocSelected: (docId: string) => boolean;
|
||||
getCountOfSelectedDocs: (docIds: string[]) => number;
|
||||
hasSelectedDocs: boolean;
|
||||
selectedDocIds: string[];
|
||||
toggleDocSelection: (docId: string) => void;
|
||||
selectAllDocs: () => void;
|
||||
selectMoreDocs: (docIds: string[]) => void;
|
||||
deselectSomeDocs: (docIds: string[]) => void;
|
||||
replaceSelectedDocs: (docIds: string[]) => void;
|
||||
clearAllSelectedDocs: () => void;
|
||||
}
|
||||
|
||||
export const useSelectedDocs = (docMap: Map<string, DataTableRecord>): UseSelectedDocsState => {
|
||||
const [selectedDocsSet, setSelectedDocsSet] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleDocSelection = useCallback((docId: string) => {
|
||||
setSelectedDocsSet((prevSelectedRowsSet) => {
|
||||
const newSelectedRowsSet = new Set(prevSelectedRowsSet);
|
||||
if (newSelectedRowsSet.has(docId)) {
|
||||
newSelectedRowsSet.delete(docId);
|
||||
} else {
|
||||
newSelectedRowsSet.add(docId);
|
||||
}
|
||||
return newSelectedRowsSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const replaceSelectedDocs = useCallback((docIds: string[]) => {
|
||||
setSelectedDocsSet(new Set(docIds));
|
||||
}, []);
|
||||
|
||||
const selectAllDocs = useCallback(() => {
|
||||
setSelectedDocsSet(new Set(docMap.keys()));
|
||||
}, [docMap]);
|
||||
|
||||
const selectMoreDocs = useCallback((docIds: string[]) => {
|
||||
setSelectedDocsSet((prevSelectedRowsSet) => new Set([...prevSelectedRowsSet, ...docIds]));
|
||||
}, []);
|
||||
|
||||
const deselectSomeDocs = useCallback((docIds: string[]) => {
|
||||
setSelectedDocsSet(
|
||||
(prevSelectedRowsSet) =>
|
||||
new Set([...prevSelectedRowsSet].filter((docId) => !docIds.includes(docId)))
|
||||
);
|
||||
}, []);
|
||||
|
||||
const clearAllSelectedDocs = useCallback(() => {
|
||||
setSelectedDocsSet(new Set());
|
||||
}, []);
|
||||
|
||||
const selectedDocIds = useMemo(
|
||||
() => Array.from(selectedDocsSet).filter((docId) => docMap.has(docId)),
|
||||
[selectedDocsSet, docMap]
|
||||
);
|
||||
|
||||
const isDocSelected = useCallback(
|
||||
(docId: string) => selectedDocsSet.has(docId) && docMap.has(docId),
|
||||
[selectedDocsSet, docMap]
|
||||
);
|
||||
|
||||
const usedSelectedDocsCount = selectedDocIds.length;
|
||||
|
||||
const getCountOfSelectedDocs = useCallback(
|
||||
(docIds) => {
|
||||
if (!usedSelectedDocsCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return docIds.filter(isDocSelected).length;
|
||||
},
|
||||
[usedSelectedDocsCount, isDocSelected]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
isDocSelected,
|
||||
hasSelectedDocs: usedSelectedDocsCount > 0,
|
||||
getCountOfSelectedDocs,
|
||||
selectedDocIds,
|
||||
toggleDocSelection,
|
||||
selectAllDocs,
|
||||
selectMoreDocs,
|
||||
deselectSomeDocs,
|
||||
replaceSelectedDocs,
|
||||
clearAllSelectedDocs,
|
||||
}),
|
||||
[
|
||||
isDocSelected,
|
||||
getCountOfSelectedDocs,
|
||||
toggleDocSelection,
|
||||
selectAllDocs,
|
||||
selectMoreDocs,
|
||||
deselectSomeDocs,
|
||||
replaceSelectedDocs,
|
||||
clearAllSelectedDocs,
|
||||
usedSelectedDocsCount,
|
||||
selectedDocIds,
|
||||
]
|
||||
);
|
||||
};
|
|
@ -11,6 +11,7 @@ import type { DataView } from '@kbn/data-views-plugin/public';
|
|||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import type { ValueToStringConverter } from './types';
|
||||
import type { UseSelectedDocsState } from './hooks/use_selected_docs';
|
||||
|
||||
export interface DataTableContext {
|
||||
expanded?: DataTableRecord | undefined;
|
||||
|
@ -19,11 +20,12 @@ export interface DataTableContext {
|
|||
onFilter?: DocViewFilterFn;
|
||||
dataView: DataView;
|
||||
isDarkMode: boolean;
|
||||
selectedDocs: string[];
|
||||
setSelectedDocs: (selected: string[]) => void;
|
||||
selectedDocsState: UseSelectedDocsState;
|
||||
valueToStringConverter: ValueToStringConverter;
|
||||
componentsTourSteps?: Record<string, string>;
|
||||
isPlainRecord?: boolean;
|
||||
pageIndex: number | undefined; // undefined when the pagination is disabled
|
||||
pageSize: number | undefined;
|
||||
}
|
||||
|
||||
const defaultContext = {} as unknown as DataTableContext;
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const dataGrid = getService('dataGrid');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const retry = getService('retry');
|
||||
const security = getService('security');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'dashboard']);
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
const defaultSettings = {
|
||||
defaultIndex: 'logstash-*',
|
||||
'discover:sampleRowsPerPage': PAGE_SIZE,
|
||||
hideAnnouncements: true,
|
||||
};
|
||||
|
||||
describe('discover data grid row selection', function describeIndexTests() {
|
||||
before(async function () {
|
||||
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
|
||||
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
|
||||
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
|
||||
await kibanaServer.uiSettings.replace(defaultSettings);
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await kibanaServer.uiSettings.replace({});
|
||||
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
|
||||
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('should be able to select rows manually', async () => {
|
||||
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(false);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
await dataGrid.selectRow(2);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// deselect
|
||||
await dataGrid.selectRow(2);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to bulk select rows', async () => {
|
||||
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(false);
|
||||
expect(await testSubjects.getAttribute('selectAllDocsOnPageToggle', 'title')).to.be(
|
||||
'Select all visible rows'
|
||||
);
|
||||
|
||||
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);
|
||||
expect(await testSubjects.getAttribute('selectAllDocsOnPageToggle', 'title')).to.be(
|
||||
'Deselect all visible rows'
|
||||
);
|
||||
});
|
||||
|
||||
await dataGrid.toggleSelectAllRowsOnCurrentPage();
|
||||
|
||||
await retry.try(async () => {
|
||||
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(false);
|
||||
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(0);
|
||||
expect(await testSubjects.getAttribute('selectAllDocsOnPageToggle', 'title')).to.be(
|
||||
'Select all visible rows'
|
||||
);
|
||||
});
|
||||
|
||||
await dataGrid.toggleSelectAllRowsOnCurrentPage();
|
||||
|
||||
await retry.try(async () => {
|
||||
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(true);
|
||||
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(PAGE_SIZE);
|
||||
expect(await dataGrid.getNumberOfSelectedRows()).to.be(PAGE_SIZE);
|
||||
expect(await testSubjects.getAttribute('selectAllDocsOnPageToggle', 'title')).to.be(
|
||||
'Deselect all visible rows'
|
||||
);
|
||||
expect(await testSubjects.getVisibleText('dscGridSelectAllDocs')).to.be('Select all 500');
|
||||
});
|
||||
|
||||
await dataGrid.selectAllRows();
|
||||
|
||||
await retry.try(async () => {
|
||||
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(true);
|
||||
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(PAGE_SIZE);
|
||||
expect(await dataGrid.getNumberOfSelectedRows()).to.be(500);
|
||||
expect(await testSubjects.getAttribute('selectAllDocsOnPageToggle', 'title')).to.be(
|
||||
'Deselect all visible rows'
|
||||
);
|
||||
await testSubjects.missingOrFail('dscGridSelectAllDocs');
|
||||
});
|
||||
|
||||
await dataGrid.toggleSelectAllRowsOnCurrentPage();
|
||||
|
||||
await retry.try(async () => {
|
||||
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(true);
|
||||
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(0);
|
||||
expect(await dataGrid.getNumberOfSelectedRows()).to.be(500 - PAGE_SIZE);
|
||||
expect(await testSubjects.getAttribute('selectAllDocsOnPageToggle', 'title')).to.be(
|
||||
'Select all visible rows'
|
||||
);
|
||||
await testSubjects.existOrFail('dscGridSelectAllDocs');
|
||||
});
|
||||
|
||||
await dataGrid.openSelectedRowsMenu();
|
||||
await testSubjects.click('dscGridClearSelectedDocuments');
|
||||
|
||||
await retry.try(async () => {
|
||||
expect(await dataGrid.isSelectedRowsMenuVisible()).to.be(false);
|
||||
expect(await dataGrid.getNumberOfSelectedRowsOnCurrentPage()).to.be(0);
|
||||
expect(await testSubjects.getAttribute('selectAllDocsOnPageToggle', 'title')).to.be(
|
||||
'Select all visible rows'
|
||||
);
|
||||
await testSubjects.missingOrFail('dscGridSelectAllDocs');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -22,6 +22,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
|
||||
loadTestFile(require.resolve('./_data_grid_row_navigation'));
|
||||
loadTestFile(require.resolve('./_data_grid_row_height'));
|
||||
loadTestFile(require.resolve('./_data_grid_row_selection'));
|
||||
loadTestFile(require.resolve('./_data_grid_sample_size'));
|
||||
loadTestFile(require.resolve('./_data_grid_pagination'));
|
||||
});
|
||||
|
|
|
@ -584,6 +584,36 @@ export class DataGridService extends FtrService {
|
|||
await checkbox.click();
|
||||
}
|
||||
|
||||
public async getNumberOfSelectedRows() {
|
||||
const label = await this.find.byCssSelector(
|
||||
'[data-test-subj=unifiedDataTableSelectionBtn] .euiNotificationBadge'
|
||||
);
|
||||
return Number(await label.getVisibleText());
|
||||
}
|
||||
|
||||
public async getNumberOfSelectedRowsOnCurrentPage() {
|
||||
const selectedRows = await this.find.allByCssSelector(
|
||||
'.euiDataGridRow [data-gridcell-column-id="select"] .euiCheckbox__input:checked'
|
||||
);
|
||||
return selectedRows.length;
|
||||
}
|
||||
|
||||
public async toggleSelectAllRowsOnCurrentPage() {
|
||||
const checkbox = await this.testSubjects.find('selectAllDocsOnPageToggle');
|
||||
|
||||
await checkbox.click();
|
||||
}
|
||||
|
||||
public async selectAllRows() {
|
||||
const button = await this.testSubjects.find('dscGridSelectAllDocs');
|
||||
|
||||
await button.click();
|
||||
}
|
||||
|
||||
public async isSelectedRowsMenuVisible() {
|
||||
return await this.testSubjects.exists('unifiedDataTableSelectionBtn');
|
||||
}
|
||||
|
||||
public async openSelectedRowsMenu() {
|
||||
await this.testSubjects.click('unifiedDataTableSelectionBtn');
|
||||
await this.retry.try(async () => {
|
||||
|
@ -591,11 +621,22 @@ export class DataGridService extends FtrService {
|
|||
});
|
||||
}
|
||||
|
||||
public async closeSelectedRowsMenu() {
|
||||
await this.testSubjects.click('unifiedDataTableSelectionBtn');
|
||||
await this.retry.try(async () => {
|
||||
return !(await this.testSubjects.exists('unifiedDataTableSelectionMenu'));
|
||||
});
|
||||
}
|
||||
|
||||
public async compareSelectedButtonExists() {
|
||||
return await this.testSubjects.exists('unifiedDataTableCompareSelectedDocuments');
|
||||
await this.openSelectedRowsMenu();
|
||||
const exists = await this.testSubjects.exists('unifiedDataTableCompareSelectedDocuments');
|
||||
await this.closeSelectedRowsMenu();
|
||||
return exists;
|
||||
}
|
||||
|
||||
public async clickCompareSelectedButton() {
|
||||
await this.openSelectedRowsMenu();
|
||||
await this.testSubjects.click('unifiedDataTableCompareSelectedDocuments');
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,10 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
|
|||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.udtTimeline .euiDataGridHeaderCell.euiDataGridHeaderCell--controlColumn {
|
||||
.udtTimeline
|
||||
.euiDataGridHeaderCell.euiDataGridHeaderCell--controlColumn:not(
|
||||
[data-gridcell-column-id='select']
|
||||
) {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue