Skip to content

Commit

Permalink
[Discover] [Unified Data Table] Add document comparison mode (#166577)
Browse files Browse the repository at this point in the history
## Summary

This PR adds a new document comparison mode to Discover and Unified Data
Table:

![diff](https://github.com/elastic/kibana/assets/25592674/40edf401-9514-4388-87a2-c528dbdd6f90)

Flaky test runner x50:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5590.

Resolves #93567.

### 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [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)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
davismcphee and kibanamachine authored Apr 10, 2024
1 parent 0b785fa commit 339379e
Show file tree
Hide file tree
Showing 51 changed files with 4,855 additions and 189 deletions.
37 changes: 37 additions & 0 deletions packages/kbn-discover-utils/src/__mocks__/es_hits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
* Side Public License, v 1.
*/

import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { KBN_FIELD_TYPES } from '@kbn/field-types';
import type { EsHitRecord } from '../types';

export const esHitsMock = [
{
_index: 'i',
Expand Down Expand Up @@ -54,3 +58,36 @@ export const esHitsMockWithSort = esHitsMock.map((hit) => ({
...hit,
sort: [hit._source.date], // some `sort` param should be specified for "fetch more" to work
}));

const baseDate = new Date('2024-01-1').getTime();
const dateInc = 100_000_000;

const generateFieldValue = (field: DataViewField, index: number) => {
switch (field.type) {
case KBN_FIELD_TYPES.BOOLEAN:
return index % 2 === 0;
case KBN_FIELD_TYPES.DATE:
return new Date(baseDate + index * dateInc).toISOString();
case KBN_FIELD_TYPES.NUMBER:
return Array.from(field.name).reduce((sum, char) => sum + char.charCodeAt(0) + index, 0);
case KBN_FIELD_TYPES.STRING:
return `${field.name}_${index}`;
default:
throw new Error(`Unsupported type ${field.type}`);
}
};

export const generateEsHits = (dataView: DataView, count: number): EsHitRecord[] => {
return Array.from({ length: count }, (_, i) => ({
_index: 'i',
_id: i.toString(),
_score: 1,
fields: dataView.fields.reduce<Record<string, any>>(
(source, field) => ({
...source,
[field.name]: [generateFieldValue(field, i)],
}),
{}
),
}));
};
15 changes: 10 additions & 5 deletions packages/kbn-unified-data-table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
*/

export { UnifiedDataTable, DataLoadingState } from './src/components/data_table';
export type {
UnifiedDataTableProps,
UnifiedDataTableRenderCustomToolbar,
UnifiedDataTableRenderCustomToolbarProps,
} from './src/components/data_table';
export type { UnifiedDataTableProps } from './src/components/data_table';
export {
RowHeightSettings,
type RowHeightSettingsProps,
Expand All @@ -31,3 +27,12 @@ export { popularizeField } from './src/utils/popularize_field';
export { useColumns } from './src/hooks/use_data_grid_columns';
export { OPEN_DETAILS, SELECT_ROW } from './src/components/data_table_columns';
export { DataTableRowControl } from './src/components/data_table_row_control';

export type {
UnifiedDataTableRenderCustomToolbar,
UnifiedDataTableRenderCustomToolbarProps,
} from './src/components/custom_toolbar/render_custom_toolbar';
export {
getRenderCustomToolbarWithElements,
renderCustomToolbar,
} from './src/components/custom_toolbar/render_custom_toolbar';
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* 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 type { EuiDataGridProps } from '@elastic/eui';
import { buildDataTableRecord } from '@kbn/discover-utils';
import { generateEsHits } from '@kbn/discover-utils/src/__mocks__';
import { render } from '@testing-library/react';
import { omit } from 'lodash';
import React from 'react';
import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
import CompareDocuments, { CompareDocumentsProps } from './compare_documents';
import { useComparisonFields } from './hooks/use_comparison_fields';

let mockLocalStorage: Record<string, string> = {};

jest.mock('react-use/lib/useLocalStorage', () =>
jest.fn((key: string, value: unknown) => {
mockLocalStorage[key] = JSON.stringify(value);
return [value, jest.fn()];
})
);

let mockDataGridProps: EuiDataGridProps | undefined;

jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
EuiDataGrid: jest.fn((props) => {
mockDataGridProps = props;
return <></>;
}),
}));

jest.mock('./hooks/use_comparison_fields', () => {
const originalModule = jest.requireActual('./hooks/use_comparison_fields');
return {
...originalModule,
useComparisonFields: jest.fn(originalModule.useComparisonFields),
};
});

const docs = generateEsHits(dataViewWithTimefieldMock, 5).map((hit) =>
buildDataTableRecord(hit, dataViewWithTimefieldMock)
);

const getDocById = (id: string) => docs.find((doc) => doc.raw._id === id);

const renderCompareDocuments = ({
forceShowAllFields = false,
}: { forceShowAllFields?: boolean } = {}) => {
const setSelectedDocs = jest.fn();
const getCompareDocuments = (props?: Partial<CompareDocumentsProps>) => (
<CompareDocuments
id="test"
wrapper={document.body}
consumer="test"
ariaDescribedBy="test"
ariaLabelledBy="test"
dataView={dataViewWithTimefieldMock}
isPlainRecord={false}
selectedFieldNames={['message', 'extension', 'bytes']}
selectedDocs={['0', '1', '2']}
schemaDetectors={[]}
forceShowAllFields={forceShowAllFields}
showFullScreenButton={true}
fieldFormats={{} as any}
getDocById={getDocById}
setSelectedDocs={setSelectedDocs}
setIsCompareActive={jest.fn()}
{...props}
/>
);
const { rerender } = render(getCompareDocuments());
return {
setSelectedDocs,
rerender: (props?: Partial<CompareDocumentsProps>) => rerender(getCompareDocuments(props)),
};
};

describe('CompareDocuments', () => {
beforeEach(() => {
mockLocalStorage = {};
mockDataGridProps = undefined;
});

it('should pass expected grid props', () => {
renderCompareDocuments();
expect(mockDataGridProps).toBeDefined();
expect(mockDataGridProps?.columns).toBeDefined();
expect(mockDataGridProps?.css).toBeDefined();
expect(omit(mockDataGridProps, 'columns', 'css')).toMatchInlineSnapshot(`
Object {
"aria-describedby": "test",
"aria-labelledby": "test",
"columnVisibility": Object {
"setVisibleColumns": [Function],
"visibleColumns": Array [
"fields_generated-id",
"0",
"1",
"2",
],
},
"data-test-subj": "unifiedDataTableCompareDocuments",
"gridStyle": Object {
"border": "horizontal",
"cellPadding": "l",
"fontSize": "s",
"header": "underline",
"rowHover": "highlight",
"stripes": undefined,
},
"id": "test",
"inMemory": Object {
"level": "sorting",
},
"renderCellValue": [Function],
"renderCustomToolbar": [Function],
"rowCount": 3,
"rowHeightsOptions": Object {
"defaultHeight": "auto",
},
"schemaDetectors": Array [],
"toolbarVisibility": Object {
"showColumnSelector": false,
"showDisplaySelector": false,
"showFullScreenSelector": true,
},
}
`);
});

it('should get values from local storage', () => {
renderCompareDocuments();
expect(mockLocalStorage).toEqual({
'test:dataGridComparisonDiffMode': '"basic"',
'test:dataGridComparisonShowAllFields': 'false',
'test:dataGridComparisonShowDiff': 'true',
'test:dataGridComparisonShowDiffDecorations': 'true',
'test:dataGridComparisonShowMatchingValues': 'true',
});
});

it('should set selected docs when columns change', () => {
const { setSelectedDocs } = renderCompareDocuments();
const visibleColumns = ['fields_generated-id', '0', '1', '2'];
mockDataGridProps?.columnVisibility.setVisibleColumns(visibleColumns);
expect(setSelectedDocs).toHaveBeenCalledWith(visibleColumns.slice(1));
});

it('should force show all fields when prop is true', () => {
renderCompareDocuments();
expect(useComparisonFields).toHaveBeenLastCalledWith(
expect.objectContaining({ showAllFields: false })
);
renderCompareDocuments({ forceShowAllFields: true });
expect(useComparisonFields).toHaveBeenLastCalledWith(
expect.objectContaining({ showAllFields: true })
);
});

it('should retain comparison docs when getDocById loses access to them', () => {
const { rerender } = renderCompareDocuments();
const visibleColumns = ['fields_generated-id', '0', '1', '2'];
expect(mockDataGridProps?.columnVisibility.visibleColumns).toEqual(visibleColumns);
rerender({ getDocById: () => undefined });
expect(mockDataGridProps?.columnVisibility.visibleColumns).toEqual(visibleColumns);
});
});
Loading

0 comments on commit 339379e

Please sign in to comment.