[Discover] Improve doc viewer code in Discover (#114759)

Co-authored-by: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com>
This commit is contained in:
Matthias Wilhelm 2021-10-19 16:35:40 +02:00 committed by GitHub
parent c2c08be709
commit 6a1af300f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 135 additions and 101 deletions

View file

@ -10,12 +10,14 @@ import { IndexPatternsService } from '../../../data/common';
import { indexPatternMock } from './index_pattern';
export const indexPatternsMock = {
getCache: () => {
getCache: async () => {
return [indexPatternMock];
},
get: (id: string) => {
get: async (id: string) => {
if (id === 'the-index-pattern-id') {
return indexPatternMock;
return Promise.resolve(indexPatternMock);
} else if (id === 'invalid-index-pattern-id') {
return Promise.reject('Invald');
}
},
updateSavedObject: jest.fn(),

View file

@ -8,6 +8,8 @@
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DiscoverServices } from '../../../build_services';
import { ContextApp } from './context_app';
import { getRootBreadcrumbs } from '../../helpers/breadcrumbs';
@ -43,7 +45,29 @@ export function ContextAppRoute(props: ContextAppProps) {
]);
}, [chrome]);
const indexPattern = useIndexPattern(services.indexPatterns, indexPatternId);
const { indexPattern, error } = useIndexPattern(services.indexPatterns, indexPatternId);
if (error) {
return (
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={
<FormattedMessage
id="discover.singleDocRoute.errorTitle"
defaultMessage="An error occured"
/>
}
body={
<FormattedMessage
id="discover.singleDocRoute.errorMessage"
defaultMessage="No matching index pattern for id {indexPatternId}"
values={{ indexPatternId }}
/>
}
/>
);
}
if (!indexPattern) {
return <LoadingIndicator />;

View file

@ -14,6 +14,7 @@ import { ReactWrapper } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test';
import { Doc, DocProps } from './doc';
import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../../common';
import { indexPatternMock } from '../../../../__mocks__/index_pattern';
const mockSearchApi = jest.fn();
@ -74,21 +75,11 @@ const waitForPromises = async () =>
* this works but logs ugly error messages until we're using React 16.9
* should be adapted when we upgrade
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function mountDoc(update = false, indexPatternGetter: any = null) {
const indexPattern = {
getComputedFields: () => [],
};
const indexPatternService = {
get: indexPatternGetter ? indexPatternGetter : jest.fn(() => Promise.resolve(indexPattern)),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
async function mountDoc(update = false) {
const props = {
id: '1',
index: 'index1',
indexPatternId: 'xyz',
indexPatternService,
indexPattern: indexPatternMock,
} as DocProps;
let comp!: ReactWrapper;
await act(async () => {
@ -108,12 +99,6 @@ describe('Test of <Doc /> of Discover', () => {
expect(findTestSubject(comp, 'doc-msg-loading').length).toBe(1);
});
test('renders IndexPattern notFound msg', async () => {
const indexPatternGetter = jest.fn(() => Promise.reject({ savedObjectId: '007' }));
const comp = await mountDoc(true, indexPatternGetter);
expect(findTestSubject(comp, 'doc-msg-notFoundIndexPattern').length).toBe(1);
});
test('renders notFound msg', async () => {
mockSearchApi.mockImplementation(() => throwError({ status: 404 }));
const comp = await mountDoc(true);

View file

@ -9,7 +9,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui';
import { IndexPatternsContract } from 'src/plugins/data/public';
import { IndexPattern } from 'src/plugins/data/public';
import { getServices } from '../../../../kibana_services';
import { DocViewer } from '../../../components/doc_viewer/doc_viewer';
import { ElasticRequestState } from '../types';
@ -25,14 +25,9 @@ export interface DocProps {
*/
index: string;
/**
* IndexPattern ID used to get IndexPattern entity
* that's used for adding additional fields (stored_fields, script_fields, docvalue_fields)
* IndexPattern entity
*/
indexPatternId: string;
/**
* IndexPatternService to get a given index pattern by ID
*/
indexPatternService: IndexPatternsContract;
indexPattern: IndexPattern;
/**
* If set, will always request source, regardless of the global `fieldsFromSource` setting
*/
@ -40,7 +35,8 @@ export interface DocProps {
}
export function Doc(props: DocProps) {
const [reqState, hit, indexPattern] = useEsDocSearch(props);
const { indexPattern } = props;
const [reqState, hit] = useEsDocSearch(props);
const indexExistsLink = getServices().docLinks.links.apis.indexExists;
return (
<EuiPage>
@ -54,7 +50,7 @@ export function Doc(props: DocProps) {
<FormattedMessage
id="discover.doc.failedToLocateIndexPattern"
defaultMessage="No index pattern matches ID {indexPatternId}."
values={{ indexPatternId: props.indexPatternId }}
values={{ indexPatternId: indexPattern.id }}
/>
}
/>

View file

@ -7,6 +7,8 @@
*/
import React, { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DiscoverServices } from '../../../build_services';
import { getRootBreadcrumbs } from '../../helpers/breadcrumbs';
import { Doc } from './components/doc';
@ -31,7 +33,7 @@ function useQuery() {
export function SingleDocRoute(props: SingleDocRouteProps) {
const { services } = props;
const { chrome, timefilter, indexPatterns } = services;
const { chrome, timefilter } = services;
const { indexPatternId, index } = useParams<DocUrlParams>();
@ -52,7 +54,29 @@ export function SingleDocRoute(props: SingleDocRouteProps) {
timefilter.disableTimeRangeSelector();
});
const indexPattern = useIndexPattern(services.indexPatterns, indexPatternId);
const { indexPattern, error } = useIndexPattern(services.indexPatterns, indexPatternId);
if (error) {
return (
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={
<FormattedMessage
id="discover.singleDocRoute.errorTitle"
defaultMessage="An error occured"
/>
}
body={
<FormattedMessage
id="discover.singleDocRoute.errorMessage"
defaultMessage="No matching index pattern for id {indexPatternId}"
values={{ indexPatternId }}
/>
}
/>
);
}
if (!indexPattern) {
return <LoadingIndicator />;
@ -60,12 +84,7 @@ export function SingleDocRoute(props: SingleDocRouteProps) {
return (
<div className="app-container">
<Doc
id={docId}
index={index}
indexPatternId={indexPatternId}
indexPatternService={indexPatterns}
/>
<Doc id={docId} index={index} indexPattern={indexPattern} />
</div>
);
}

View file

@ -10,6 +10,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { DocViewerTab } from './doc_viewer_tab';
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
import { indexPatternMock } from '../../../__mocks__/index_pattern';
describe('DocViewerTab', () => {
test('changing columns triggers an update', () => {
@ -21,6 +22,7 @@ describe('DocViewerTab', () => {
renderProps: {
hit: {} as ElasticSearchHit,
columns: ['test'],
indexPattern: indexPatternMock,
},
};
@ -31,6 +33,7 @@ describe('DocViewerTab', () => {
renderProps: {
hit: {} as ElasticSearchHit,
columns: ['test2'],
indexPattern: indexPatternMock,
},
};

View file

@ -5,7 +5,11 @@ exports[`Source Viewer component renders error state 1`] = `
hasLineNumbers={true}
id="1"
index="index1"
indexPatternId="xyz"
indexPattern={
Object {
"getComputedFields": [Function],
}
}
intl={
Object {
"defaultFormats": Object {},
@ -264,7 +268,11 @@ exports[`Source Viewer component renders json code editor 1`] = `
hasLineNumbers={true}
id="1"
index="index1"
indexPatternId="xyz"
indexPattern={
Object {
"getComputedFields": [Function],
}
}
intl={
Object {
"defaultFormats": Object {},
@ -619,7 +627,11 @@ exports[`Source Viewer component renders loading state 1`] = `
hasLineNumbers={true}
id="1"
index="index1"
indexPatternId="xyz"
indexPattern={
Object {
"getComputedFields": [Function],
}
}
intl={
Object {
"defaultFormats": Object {},

View file

@ -43,13 +43,13 @@ const mockIndexPatternService = {
}));
describe('Source Viewer component', () => {
test('renders loading state', () => {
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, null, () => {}]);
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, () => {}]);
const comp = mountWithIntl(
<SourceViewer
id={'1'}
index={'index1'}
indexPatternId={'xyz'}
indexPattern={mockIndexPattern}
width={123}
hasLineNumbers={true}
/>
@ -60,13 +60,13 @@ describe('Source Viewer component', () => {
});
test('renders error state', () => {
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, null, () => {}]);
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, () => {}]);
const comp = mountWithIntl(
<SourceViewer
id={'1'}
index={'index1'}
indexPatternId={'xyz'}
indexPattern={mockIndexPattern}
width={123}
hasLineNumbers={true}
/>
@ -97,9 +97,7 @@ describe('Source Viewer component', () => {
_underscore: 123,
},
} as never;
jest
.spyOn(hooks, 'useEsDocSearch')
.mockImplementation(() => [2, mockHit, mockIndexPattern, () => {}]);
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [2, mockHit, () => {}]);
jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => {
return false;
});
@ -107,7 +105,7 @@ describe('Source Viewer component', () => {
<SourceViewer
id={'1'}
index={'index1'}
indexPatternId={'xyz'}
indexPattern={mockIndexPattern}
width={123}
hasLineNumbers={true}
/>

View file

@ -17,11 +17,12 @@ import { getServices } from '../../../kibana_services';
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
import { ElasticRequestState } from '../../apps/doc/types';
import { useEsDocSearch } from '../../services/use_es_doc_search';
import { IndexPattern } from '../../../../../data_views/common';
interface SourceViewerProps {
id: string;
index: string;
indexPatternId: string;
indexPattern: IndexPattern;
hasLineNumbers: boolean;
width?: number;
}
@ -29,19 +30,17 @@ interface SourceViewerProps {
export const SourceViewer = ({
id,
index,
indexPatternId,
indexPattern,
width,
hasLineNumbers,
}: SourceViewerProps) => {
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor>();
const [jsonValue, setJsonValue] = useState<string>('');
const indexPatternService = getServices().data.indexPatterns;
const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
const [reqState, hit, , requestData] = useEsDocSearch({
const [reqState, hit, requestData] = useEsDocSearch({
id,
index,
indexPatternId,
indexPatternService,
indexPattern,
requestSource: useNewFieldsApi,
});
@ -106,11 +105,7 @@ export const SourceViewer = ({
<EuiEmptyPrompt iconType="alert" title={errorMessageTitle} body={errorMessage} />
);
if (
reqState === ElasticRequestState.Error ||
reqState === ElasticRequestState.NotFound ||
reqState === ElasticRequestState.NotFoundIndexPattern
) {
if (reqState === ElasticRequestState.Error || reqState === ElasticRequestState.NotFound) {
return errorState;
}

View file

@ -25,7 +25,7 @@ export interface DocViewerTableProps {
columns?: string[];
filter?: DocViewFilterFn;
hit: ElasticSearchHit;
indexPattern?: IndexPattern;
indexPattern: IndexPattern;
onAddColumn?: (columnName: string) => void;
onRemoveColumn?: (columnName: string) => void;
}

View file

@ -32,7 +32,7 @@ export interface DocViewRenderProps {
columns?: string[];
filter?: DocViewFilterFn;
hit: ElasticSearchHit;
indexPattern?: IndexPattern;
indexPattern: IndexPattern;
onAddColumn?: (columnName: string) => void;
onRemoveColumn?: (columnName: string) => void;
}

View file

@ -8,12 +8,24 @@
import { useIndexPattern } from './use_index_pattern';
import { indexPatternMock } from '../../__mocks__/index_pattern';
import { indexPatternsMock } from '../../__mocks__/index_patterns';
import { renderHook, act } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks';
describe('Use Index Pattern', () => {
test('returning a valid index pattern', async () => {
const { result } = renderHook(() => useIndexPattern(indexPatternsMock, 'the-index-pattern-id'));
await act(() => Promise.resolve());
expect(result.current).toBe(indexPatternMock);
const { result, waitForNextUpdate } = renderHook(() =>
useIndexPattern(indexPatternsMock, 'the-index-pattern-id')
);
await waitForNextUpdate();
expect(result.current.indexPattern).toBe(indexPatternMock);
expect(result.current.error).toBe(undefined);
});
test('returning an error', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useIndexPattern(indexPatternsMock, 'invalid-index-pattern-id')
);
await waitForNextUpdate();
expect(result.current.indexPattern).toBe(undefined);
expect(result.current.error).toBeTruthy();
});
});

View file

@ -10,13 +10,18 @@ import { IndexPattern, IndexPatternsContract } from '../../../../data/common';
export const useIndexPattern = (indexPatterns: IndexPatternsContract, indexPatternId: string) => {
const [indexPattern, setIndexPattern] = useState<IndexPattern | undefined>(undefined);
const [error, setError] = useState();
useEffect(() => {
async function loadIndexPattern() {
const ip = await indexPatterns.get(indexPatternId);
setIndexPattern(ip);
try {
const item = await indexPatterns.get(indexPatternId);
setIndexPattern(item);
} catch (e) {
setError(e);
}
}
loadIndexPattern();
});
return indexPattern;
}, [indexPatternId, indexPatterns]);
return { indexPattern, error };
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks';
import { buildSearchBody, useEsDocSearch } from './use_es_doc_search';
import { Observable } from 'rxjs';
import { IndexPattern } from 'src/plugins/data/common';
@ -175,26 +175,14 @@ describe('Test of <Doc /> helper / hook', () => {
const indexPattern = {
getComputedFields: () => [],
};
const getMock = jest.fn(() => Promise.resolve(indexPattern));
const indexPatternService = {
get: getMock,
} as unknown as IndexPattern;
const props = {
id: '1',
index: 'index1',
indexPatternId: 'xyz',
indexPatternService,
} as unknown as DocProps;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let hook: any;
await act(async () => {
hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props });
});
expect(hook.result.current.slice(0, 3)).toEqual([
ElasticRequestState.Loading,
null,
indexPattern,
]);
expect(getMock).toHaveBeenCalled();
} as unknown as DocProps;
const { result } = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props });
expect(result.current.slice(0, 2)).toEqual([ElasticRequestState.Loading, null]);
});
});

View file

@ -64,11 +64,9 @@ export function buildSearchBody(
export function useEsDocSearch({
id,
index,
indexPatternId,
indexPatternService,
indexPattern,
requestSource,
}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null, () => void] {
const [indexPattern, setIndexPattern] = useState<IndexPattern | null>(null);
}: DocProps): [ElasticRequestState, ElasticSearchHit | null | null, () => void] {
const [status, setStatus] = useState(ElasticRequestState.Loading);
const [hit, setHit] = useState<ElasticSearchHit | null>(null);
const { data, uiSettings } = useMemo(() => getServices(), []);
@ -76,14 +74,11 @@ export function useEsDocSearch({
const requestData = useCallback(async () => {
try {
const indexPatternEntity = await indexPatternService.get(indexPatternId);
setIndexPattern(indexPatternEntity);
const { rawResponse } = await data.search
.search({
params: {
index,
body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi, requestSource)?.body,
body: buildSearchBody(id, indexPattern, useNewFieldsApi, requestSource)?.body,
},
})
.toPromise();
@ -105,11 +100,11 @@ export function useEsDocSearch({
setStatus(ElasticRequestState.Error);
}
}
}, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi, requestSource]);
}, [id, index, indexPattern, data.search, useNewFieldsApi, requestSource]);
useEffect(() => {
requestData();
}, [requestData]);
return [status, hit, indexPattern, requestData];
return [status, hit, requestData];
}

View file

@ -267,7 +267,7 @@ export class DiscoverPlugin
<SourceViewer
index={hit._index}
id={hit._id}
indexPatternId={indexPattern?.id || ''}
indexPattern={indexPattern}
hasLineNumbers
/>
</React.Suspense>