[Discover][ES|QL] Fix JSON view for ES|QL record in DocViewer (#216642)

- Closes https://github.com/elastic/kibana/issues/214805

## Summary

By default ES|QL records don't have `_id` unless it's requested via the
query `METADATA`.
This PR fixes the JSON view inside DocViewer for ES|QL records.
Previously it was relying on `textBasedHits` which gets updated when
query changes hence there is a possibility of loosing the reference to
the last viewed record.

## Testing

Example queries:
```
FROM kibana_sample_data_ecommerce METADATA _index
FROM kibana_sample_data_ecommerce METADATA _index, _id
```

### Checklist


- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Julia Rechkunova 2025-04-02 17:59:03 +02:00 committed by GitHub
parent cc09a96efe
commit 43b6cc4c1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 58 additions and 25 deletions

View file

@ -16,6 +16,10 @@ import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/u
import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
import { JsonCodeEditorCommon } from '../json_code_editor';
import { buildDataTableRecord } from '@kbn/discover-utils';
import { mockUnifiedDocViewerServices } from '../../__mocks__';
import { setUnifiedDocViewerServices } from '../../plugin';
setUnifiedDocViewerServices(mockUnifiedDocViewerServices);
const mockDataView = {
getComputedFields: () => [],
@ -94,4 +98,35 @@ describe('Source Viewer component', () => {
expect(jsonCodeEditor.props().hasLineNumbers).toBe(true);
expect(jsonCodeEditor.props().enableFindAction).toBe(true);
});
test('renders json code editor for ES|QL record', () => {
const record = {
_index: 'logstash-2014.09.09',
_id: 'id123',
message: 'Lorem ipsum dolor sit amet',
extension: 'html',
};
const mockHit = {
id: '22',
raw: record,
flattened: record,
};
jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => {
return false;
});
const comp = mountWithIntl(
<DocViewerSource
id={mockHit.id}
index={'index1'}
dataView={mockDataView}
esqlHit={mockHit}
width={123}
onRefresh={() => {}}
/>
);
const jsonCodeEditor = comp.find(JsonCodeEditorCommon);
expect(jsonCodeEditor).not.toBe(null);
expect(jsonCodeEditor.props().jsonValue).toContain('message');
expect(jsonCodeEditor.props().jsonValue).toContain('_id');
});
});

View file

@ -26,7 +26,7 @@ interface SourceViewerProps {
id: string;
index: string | undefined;
dataView: DataView;
textBasedHits?: DataTableRecord[];
esqlHit?: DataTableRecord;
width?: number;
decreaseAvailableHeightBy?: number;
onRefresh: () => void;
@ -39,7 +39,7 @@ export const DocViewerSource = ({
id,
index,
dataView,
textBasedHits,
esqlHit,
width,
decreaseAvailableHeightBy,
onRefresh,
@ -51,7 +51,7 @@ export const DocViewerSource = ({
id,
index,
dataView,
textBasedHits,
esqlHit,
});
useEffect(() => {

View file

@ -12,7 +12,7 @@ import { type EsDocSearchProps, buildSearchBody, useEsDocSearch } from './use_es
import { Subject } from 'rxjs';
import type { DataView } from '@kbn/data-views-plugin/public';
import { ElasticRequestState } from '@kbn/unified-doc-viewer';
import { buildDataTableRecord } from '@kbn/discover-utils';
import { buildDataTableRecord, DataTableRecord } from '@kbn/discover-utils';
import { setUnifiedDocViewerServices } from '../plugin';
import { UnifiedDocViewerServices } from '../types';
@ -202,18 +202,16 @@ describe('Test of <Doc /> helper / hook', () => {
getComputedFields: () => [],
getIndexPattern: () => index,
};
const props = {
const props: EsDocSearchProps = {
id: '1',
index: 'index1',
dataView,
textBasedHits: [
{
id: '1',
raw: { field1: 1, field2: 2 },
flattened: { field1: 1, field2: 2 },
},
],
} as unknown as EsDocSearchProps;
dataView: dataView as unknown as EsDocSearchProps['dataView'],
esqlHit: {
id: '1',
raw: { field1: 1, field2: 2 },
flattened: { field1: 1, field2: 2 },
} as DataTableRecord,
};
const hook = renderHook((p: EsDocSearchProps) => useEsDocSearch(p), {
initialProps: props,

View file

@ -33,9 +33,9 @@ export interface EsDocSearchProps {
*/
dataView: DataView;
/**
* Records fetched from text based query
* Record fetched from ES|QL query
*/
textBasedHits?: DataTableRecord[];
esqlHit?: DataTableRecord;
/**
* An optional callback that will be called before fetching the doc
*/
@ -54,7 +54,7 @@ export function useEsDocSearch({
id,
index,
dataView,
textBasedHits,
esqlHit,
onBeforeFetch,
onProcessRecord,
}: EsDocSearchProps): [ElasticRequestState, DataTableRecord | null, () => void] {
@ -111,16 +111,13 @@ export function useEsDocSearch({
}, [analytics, data.search, dataView, id, index, onBeforeFetch, onProcessRecord]);
useEffect(() => {
if (textBasedHits) {
const selectedHit = textBasedHits?.find((r) => r.id === id);
if (selectedHit) {
setStatus(ElasticRequestState.Found);
setHit(selectedHit);
}
if (esqlHit) {
setStatus(ElasticRequestState.Found);
setHit(esqlHit);
} else {
requestData();
}
}, [id, requestData, textBasedHits]);
}, [id, requestData, esqlHit]);
return [status, hit, requestData];
}

View file

@ -77,7 +77,10 @@ export class UnifiedDocViewerPublicPlugin
index={hit.raw._index}
id={hit.raw._id ?? hit.id}
dataView={dataView}
textBasedHits={textBasedHits}
// If ES|QL query changes, then textBasedHits will update too.
// This is a workaround to reuse the previously referred hit
// so the doc viewer preserves the state even after the record disappears from hits list.
esqlHit={Array.isArray(textBasedHits) ? hit : undefined}
decreaseAvailableHeightBy={decreaseAvailableHeightBy}
onRefresh={() => {}}
/>