mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* [Discover] Add source to doc viewer * Updating a unit test * Fix typescript errors * Add unit test * Add a functional test * Fixing a typo * Remove unnecessary import * Always request fields and source * Remove unused import * Move initialization of SourceViewer back to setup * Trying to get rid of null value * Readding null * Try to get rid of null value * Addressing PR comments * Return early if jsonValue is not set * Fix loading spinner style * Add refresh on error * Fix error message * Add loading indicator on an empty string Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
fd15a1f5f8
commit
0e28661990
15 changed files with 1248 additions and 157 deletions
|
@ -10,9 +10,10 @@ import React from 'react';
|
|||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui';
|
||||
import { IndexPatternsContract } from 'src/plugins/data/public';
|
||||
import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search';
|
||||
import { useEsDocSearch } from './use_es_doc_search';
|
||||
import { getServices } from '../../../kibana_services';
|
||||
import { DocViewer } from '../doc_viewer/doc_viewer';
|
||||
import { ElasticRequestState } from './elastic_request_state';
|
||||
|
||||
export interface DocProps {
|
||||
/**
|
||||
|
@ -32,6 +33,10 @@ export interface DocProps {
|
|||
* IndexPatternService to get a given index pattern by ID
|
||||
*/
|
||||
indexPatternService: IndexPatternsContract;
|
||||
/**
|
||||
* If set, will always request source, regardless of the global `fieldsFromSource` setting
|
||||
*/
|
||||
requestSource?: boolean;
|
||||
}
|
||||
|
||||
export function Doc(props: DocProps) {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum ElasticRequestState {
|
||||
Loading,
|
||||
NotFound,
|
||||
Found,
|
||||
Error,
|
||||
NotFoundIndexPattern,
|
||||
}
|
|
@ -7,11 +7,12 @@
|
|||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_doc_search';
|
||||
import { buildSearchBody, useEsDocSearch } from './use_es_doc_search';
|
||||
import { DocProps } from './doc';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common';
|
||||
import { IndexPattern } from 'src/plugins/data/common';
|
||||
import { ElasticRequestState } from './elastic_request_state';
|
||||
|
||||
const mockSearchResult = new Observable();
|
||||
|
||||
|
@ -88,6 +89,36 @@ describe('Test of <Doc /> helper / hook', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('buildSearchBody with requestSource', () => {
|
||||
const indexPattern = ({
|
||||
getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }),
|
||||
} as unknown) as IndexPattern;
|
||||
const actual = buildSearchBody('1', indexPattern, true, true);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"_source": true,
|
||||
"fields": Array [
|
||||
Object {
|
||||
"field": "*",
|
||||
"include_unmapped": "true",
|
||||
},
|
||||
],
|
||||
"query": Object {
|
||||
"ids": Object {
|
||||
"values": Array [
|
||||
"1",
|
||||
],
|
||||
},
|
||||
},
|
||||
"runtime_mappings": Object {},
|
||||
"script_fields": Array [],
|
||||
"stored_fields": Array [],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('buildSearchBody with runtime fields', () => {
|
||||
const indexPattern = ({
|
||||
getComputedFields: () => ({
|
||||
|
@ -155,7 +186,11 @@ describe('Test of <Doc /> helper / hook', () => {
|
|||
await act(async () => {
|
||||
hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props });
|
||||
});
|
||||
expect(hook.result.current).toEqual([ElasticRequestState.Loading, null, indexPattern]);
|
||||
expect(hook.result.current.slice(0, 3)).toEqual([
|
||||
ElasticRequestState.Loading,
|
||||
null,
|
||||
indexPattern,
|
||||
]);
|
||||
expect(getMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,23 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import { IndexPattern, getServices } from '../../../kibana_services';
|
||||
import { getServices, IndexPattern } from '../../../kibana_services';
|
||||
import { DocProps } from './doc';
|
||||
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
|
||||
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
|
||||
import { ElasticRequestState } from './elastic_request_state';
|
||||
|
||||
type RequestBody = Pick<estypes.SearchRequest, 'body'>;
|
||||
|
||||
export enum ElasticRequestState {
|
||||
Loading,
|
||||
NotFound,
|
||||
Found,
|
||||
Error,
|
||||
NotFoundIndexPattern,
|
||||
}
|
||||
|
||||
/**
|
||||
* helper function to build a query body for Elasticsearch
|
||||
* https://www.elastic.co/guide/en/elasticsearch/reference/current//query-dsl-ids-query.html
|
||||
|
@ -30,7 +23,8 @@ export enum ElasticRequestState {
|
|||
export function buildSearchBody(
|
||||
id: string,
|
||||
indexPattern: IndexPattern,
|
||||
useNewFieldsApi: boolean
|
||||
useNewFieldsApi: boolean,
|
||||
requestAllFields?: boolean
|
||||
): RequestBody | undefined {
|
||||
const computedFields = indexPattern.getComputedFields();
|
||||
const runtimeFields = computedFields.runtimeFields as estypes.MappingRuntimeFields;
|
||||
|
@ -52,6 +46,9 @@ export function buildSearchBody(
|
|||
// @ts-expect-error
|
||||
request.body.fields = [{ field: '*', include_unmapped: 'true' }];
|
||||
request.body.runtime_mappings = runtimeFields ? runtimeFields : {};
|
||||
if (requestAllFields) {
|
||||
request.body._source = true;
|
||||
}
|
||||
} else {
|
||||
request.body._source = true;
|
||||
}
|
||||
|
@ -67,47 +64,50 @@ export function useEsDocSearch({
|
|||
index,
|
||||
indexPatternId,
|
||||
indexPatternService,
|
||||
}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null] {
|
||||
requestSource,
|
||||
}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null, () => void] {
|
||||
const [indexPattern, setIndexPattern] = useState<IndexPattern | null>(null);
|
||||
const [status, setStatus] = useState(ElasticRequestState.Loading);
|
||||
const [hit, setHit] = useState<ElasticSearchHit | null>(null);
|
||||
const { data, uiSettings } = useMemo(() => getServices(), []);
|
||||
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
async function requestData() {
|
||||
try {
|
||||
const indexPatternEntity = await indexPatternService.get(indexPatternId);
|
||||
setIndexPattern(indexPatternEntity);
|
||||
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)?.body,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
const { rawResponse } = await data.search
|
||||
.search({
|
||||
params: {
|
||||
index,
|
||||
body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi, requestSource)?.body,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
const hits = rawResponse.hits;
|
||||
const hits = rawResponse.hits;
|
||||
|
||||
if (hits?.hits?.[0]) {
|
||||
setStatus(ElasticRequestState.Found);
|
||||
setHit(hits.hits[0]);
|
||||
} else {
|
||||
setStatus(ElasticRequestState.NotFound);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.savedObjectId) {
|
||||
setStatus(ElasticRequestState.NotFoundIndexPattern);
|
||||
} else if (err.status === 404) {
|
||||
setStatus(ElasticRequestState.NotFound);
|
||||
} else {
|
||||
setStatus(ElasticRequestState.Error);
|
||||
}
|
||||
if (hits?.hits?.[0]) {
|
||||
setStatus(ElasticRequestState.Found);
|
||||
setHit(hits.hits[0]);
|
||||
} else {
|
||||
setStatus(ElasticRequestState.NotFound);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.savedObjectId) {
|
||||
setStatus(ElasticRequestState.NotFoundIndexPattern);
|
||||
} else if (err.status === 404) {
|
||||
setStatus(ElasticRequestState.NotFound);
|
||||
} else {
|
||||
setStatus(ElasticRequestState.Error);
|
||||
}
|
||||
}
|
||||
}, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi, requestSource]);
|
||||
|
||||
useEffect(() => {
|
||||
requestData();
|
||||
}, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi]);
|
||||
return [status, hit, indexPattern];
|
||||
}, [requestData]);
|
||||
|
||||
return [status, hit, indexPattern, requestData];
|
||||
}
|
||||
|
|
|
@ -1,21 +1,8 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`returns the \`JsonCodeEditor\` component 1`] = `
|
||||
<EuiFlexGroup
|
||||
className="dscJsonCodeEditor"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<div
|
||||
className="eui-textRight"
|
||||
>
|
||||
<EuiCopy
|
||||
afterMessage="Copied"
|
||||
textToCopy="{
|
||||
<JsonCodeEditorCommon
|
||||
jsonValue="{
|
||||
\\"_index\\": \\"test\\",
|
||||
\\"_type\\": \\"doc\\",
|
||||
\\"_id\\": \\"foo\\",
|
||||
|
@ -24,45 +11,6 @@ exports[`returns the \`JsonCodeEditor\` component 1`] = `
|
|||
\\"test\\": 123
|
||||
}
|
||||
}"
|
||||
>
|
||||
<Component />
|
||||
</EuiCopy>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CodeEditor
|
||||
aria-label="Read only JSON view of an elasticsearch document"
|
||||
editorDidMount={[Function]}
|
||||
languageId="xjson"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Object {
|
||||
"automaticLayout": true,
|
||||
"fontSize": 12,
|
||||
"lineNumbers": "off",
|
||||
"minimap": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"overviewRulerBorder": false,
|
||||
"readOnly": true,
|
||||
"scrollBeyondLastLine": false,
|
||||
"scrollbar": Object {
|
||||
"alwaysConsumeMouseWheel": false,
|
||||
},
|
||||
"wordWrap": "on",
|
||||
"wrappingIndent": "indent",
|
||||
}
|
||||
}
|
||||
value="{
|
||||
\\"_index\\": \\"test\\",
|
||||
\\"_type\\": \\"doc\\",
|
||||
\\"_id\\": \\"foo\\",
|
||||
\\"_score\\": 1,
|
||||
\\"_source\\": {
|
||||
\\"test\\": 123
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
onEditorDidMount={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.dscJsonCodeEditor {
|
||||
width: 100%
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -9,17 +9,8 @@
|
|||
import './json_code_editor.scss';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { monaco, XJsonLang } from '@kbn/monaco';
|
||||
import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { CodeEditor } from '../../../../../kibana_react/public';
|
||||
|
||||
const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', {
|
||||
defaultMessage: 'Read only JSON view of an elasticsearch document',
|
||||
});
|
||||
const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', {
|
||||
defaultMessage: 'Copy to clipboard',
|
||||
});
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { JsonCodeEditorCommon } from './json_code_editor_common';
|
||||
|
||||
interface JsonCodeEditorProps {
|
||||
json: Record<string, unknown>;
|
||||
|
@ -47,45 +38,11 @@ export const JsonCodeEditor = ({ json, width, hasLineNumbers }: JsonCodeEditorPr
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup className="dscJsonCodeEditor" direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<div className="eui-textRight">
|
||||
<EuiCopy textToCopy={jsonValue}>
|
||||
{(copy) => (
|
||||
<EuiButtonEmpty size="xs" flush="right" iconType="copyClipboard" onClick={copy}>
|
||||
{copyToClipboardLabel}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CodeEditor
|
||||
languageId={XJsonLang.ID}
|
||||
width={width}
|
||||
value={jsonValue}
|
||||
onChange={() => {}}
|
||||
editorDidMount={setEditorCalculatedHeight}
|
||||
aria-label={codeEditorAriaLabel}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
fontSize: 12,
|
||||
lineNumbers: hasLineNumbers ? 'on' : 'off',
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
overviewRulerBorder: false,
|
||||
readOnly: true,
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'indent',
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<JsonCodeEditorCommon
|
||||
jsonValue={jsonValue}
|
||||
width={width}
|
||||
hasLineNumbers={hasLineNumbers}
|
||||
onEditorDidMount={setEditorCalculatedHeight}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 './json_code_editor.scss';
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { monaco, XJsonLang } from '@kbn/monaco';
|
||||
import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { CodeEditor } from '../../../../../kibana_react/public';
|
||||
|
||||
const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', {
|
||||
defaultMessage: 'Read only JSON view of an elasticsearch document',
|
||||
});
|
||||
const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', {
|
||||
defaultMessage: 'Copy to clipboard',
|
||||
});
|
||||
|
||||
interface JsonCodeEditorCommonProps {
|
||||
jsonValue: string;
|
||||
onEditorDidMount: (editor: monaco.editor.IStandaloneCodeEditor) => void;
|
||||
width?: string | number;
|
||||
hasLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
export const JsonCodeEditorCommon = ({
|
||||
jsonValue,
|
||||
width,
|
||||
hasLineNumbers,
|
||||
onEditorDidMount,
|
||||
}: JsonCodeEditorCommonProps) => {
|
||||
if (jsonValue === '') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup className="dscJsonCodeEditor" direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<div className="eui-textRight">
|
||||
<EuiCopy textToCopy={jsonValue}>
|
||||
{(copy) => (
|
||||
<EuiButtonEmpty size="xs" flush="right" iconType="copyClipboard" onClick={copy}>
|
||||
{copyToClipboardLabel}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CodeEditor
|
||||
languageId={XJsonLang.ID}
|
||||
width={width}
|
||||
value={jsonValue || ''}
|
||||
onChange={() => {}}
|
||||
editorDidMount={onEditorDidMount}
|
||||
aria-label={codeEditorAriaLabel}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
fontSize: 12,
|
||||
lineNumbers: hasLineNumbers ? 'on' : 'off',
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
overviewRulerBorder: false,
|
||||
readOnly: true,
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'indent',
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const JSONCodeEditorCommonMemoized = React.memo((props: JsonCodeEditorCommonProps) => {
|
||||
return <JsonCodeEditorCommon {...props} />;
|
||||
});
|
|
@ -0,0 +1,760 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Source Viewer component renders error state 1`] = `
|
||||
<SourceViewer
|
||||
hasLineNumbers={true}
|
||||
id="1"
|
||||
index="index1"
|
||||
indexPatternId="xyz"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {
|
||||
"date": Object {
|
||||
"full": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"long": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"medium": Object {
|
||||
"day": "numeric",
|
||||
"month": "short",
|
||||
"year": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"day": "numeric",
|
||||
"month": "numeric",
|
||||
"year": "2-digit",
|
||||
},
|
||||
},
|
||||
"number": Object {
|
||||
"currency": Object {
|
||||
"style": "currency",
|
||||
},
|
||||
"percent": Object {
|
||||
"style": "percent",
|
||||
},
|
||||
},
|
||||
"relative": Object {
|
||||
"days": Object {
|
||||
"units": "day",
|
||||
},
|
||||
"hours": Object {
|
||||
"units": "hour",
|
||||
},
|
||||
"minutes": Object {
|
||||
"units": "minute",
|
||||
},
|
||||
"months": Object {
|
||||
"units": "month",
|
||||
},
|
||||
"seconds": Object {
|
||||
"units": "second",
|
||||
},
|
||||
"years": Object {
|
||||
"units": "year",
|
||||
},
|
||||
},
|
||||
"time": Object {
|
||||
"full": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"long": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"medium": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
width={123}
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
body={
|
||||
<div>
|
||||
Could not fetch data at this time. Refresh the tab to try again.
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiButton
|
||||
iconType="refresh"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Refresh
|
||||
</EuiButton>
|
||||
</div>
|
||||
}
|
||||
iconType="alert"
|
||||
title={
|
||||
<h2>
|
||||
An Error Occurred
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiEmptyPrompt"
|
||||
>
|
||||
<EuiIcon
|
||||
color="subdued"
|
||||
size="xxl"
|
||||
type="alert"
|
||||
>
|
||||
<span
|
||||
color="subdued"
|
||||
data-euiicon-type="alert"
|
||||
size="xxl"
|
||||
/>
|
||||
</EuiIcon>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--s"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<span
|
||||
className="euiTextColor euiTextColor--subdued"
|
||||
>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
>
|
||||
<h2
|
||||
className="euiTitle euiTitle--medium"
|
||||
>
|
||||
An Error Occurred
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--m"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
<EuiText>
|
||||
<div
|
||||
className="euiText euiText--medium"
|
||||
>
|
||||
<div>
|
||||
Could not fetch data at this time. Refresh the tab to try again.
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--s"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
<EuiButton
|
||||
iconType="refresh"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<EuiButtonDisplay
|
||||
baseClassName="euiButton"
|
||||
disabled={false}
|
||||
element="button"
|
||||
iconType="refresh"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<button
|
||||
className="euiButton euiButton--primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"minWidth": undefined,
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<EuiButtonContent
|
||||
className="euiButton__content"
|
||||
iconSide="left"
|
||||
iconType="refresh"
|
||||
textProps={
|
||||
Object {
|
||||
"className": "euiButton__text",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="euiButtonContent euiButton__content"
|
||||
>
|
||||
<EuiIcon
|
||||
className="euiButtonContent__icon"
|
||||
color="inherit"
|
||||
size="m"
|
||||
type="refresh"
|
||||
>
|
||||
<span
|
||||
className="euiButtonContent__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="refresh"
|
||||
size="m"
|
||||
/>
|
||||
</EuiIcon>
|
||||
<span
|
||||
className="euiButton__text"
|
||||
>
|
||||
Refresh
|
||||
</span>
|
||||
</span>
|
||||
</EuiButtonContent>
|
||||
</button>
|
||||
</EuiButtonDisplay>
|
||||
</EuiButton>
|
||||
</div>
|
||||
</div>
|
||||
</EuiText>
|
||||
</span>
|
||||
</EuiTextColor>
|
||||
</div>
|
||||
</EuiEmptyPrompt>
|
||||
</SourceViewer>
|
||||
`;
|
||||
|
||||
exports[`Source Viewer component renders json code editor 1`] = `
|
||||
<SourceViewer
|
||||
hasLineNumbers={true}
|
||||
id="1"
|
||||
index="index1"
|
||||
indexPatternId="xyz"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {
|
||||
"date": Object {
|
||||
"full": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"long": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"medium": Object {
|
||||
"day": "numeric",
|
||||
"month": "short",
|
||||
"year": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"day": "numeric",
|
||||
"month": "numeric",
|
||||
"year": "2-digit",
|
||||
},
|
||||
},
|
||||
"number": Object {
|
||||
"currency": Object {
|
||||
"style": "currency",
|
||||
},
|
||||
"percent": Object {
|
||||
"style": "percent",
|
||||
},
|
||||
},
|
||||
"relative": Object {
|
||||
"days": Object {
|
||||
"units": "day",
|
||||
},
|
||||
"hours": Object {
|
||||
"units": "hour",
|
||||
},
|
||||
"minutes": Object {
|
||||
"units": "minute",
|
||||
},
|
||||
"months": Object {
|
||||
"units": "month",
|
||||
},
|
||||
"seconds": Object {
|
||||
"units": "second",
|
||||
},
|
||||
"years": Object {
|
||||
"units": "year",
|
||||
},
|
||||
},
|
||||
"time": Object {
|
||||
"full": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"long": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"medium": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
width={123}
|
||||
>
|
||||
<Memo()
|
||||
hasLineNumbers={true}
|
||||
jsonValue="{
|
||||
\\"_index\\": \\"logstash-2014.09.09\\",
|
||||
\\"_type\\": \\"doc\\",
|
||||
\\"_id\\": \\"id123\\",
|
||||
\\"_score\\": 1,
|
||||
\\"_source\\": {
|
||||
\\"message\\": \\"Lorem ipsum dolor sit amet\\",
|
||||
\\"extension\\": \\"html\\",
|
||||
\\"not_mapped\\": \\"yes\\",
|
||||
\\"bytes\\": 100,
|
||||
\\"objectArray\\": [
|
||||
{
|
||||
\\"foo\\": true
|
||||
}
|
||||
],
|
||||
\\"relatedContent\\": {
|
||||
\\"test\\": 1
|
||||
},
|
||||
\\"scripted\\": 123,
|
||||
\\"_underscore\\": 123
|
||||
}
|
||||
}"
|
||||
onEditorDidMount={[Function]}
|
||||
width={123}
|
||||
>
|
||||
<JsonCodeEditorCommon
|
||||
hasLineNumbers={true}
|
||||
jsonValue="{
|
||||
\\"_index\\": \\"logstash-2014.09.09\\",
|
||||
\\"_type\\": \\"doc\\",
|
||||
\\"_id\\": \\"id123\\",
|
||||
\\"_score\\": 1,
|
||||
\\"_source\\": {
|
||||
\\"message\\": \\"Lorem ipsum dolor sit amet\\",
|
||||
\\"extension\\": \\"html\\",
|
||||
\\"not_mapped\\": \\"yes\\",
|
||||
\\"bytes\\": 100,
|
||||
\\"objectArray\\": [
|
||||
{
|
||||
\\"foo\\": true
|
||||
}
|
||||
],
|
||||
\\"relatedContent\\": {
|
||||
\\"test\\": 1
|
||||
},
|
||||
\\"scripted\\": 123,
|
||||
\\"_underscore\\": 123
|
||||
}
|
||||
}"
|
||||
onEditorDidMount={[Function]}
|
||||
width={123}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
className="dscJsonCodeEditor"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionColumn euiFlexGroup--responsive dscJsonCodeEditor"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<div
|
||||
className="euiFlexItem"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--s"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
<div
|
||||
className="eui-textRight"
|
||||
>
|
||||
<EuiCopy
|
||||
afterMessage="Copied"
|
||||
textToCopy="{
|
||||
\\"_index\\": \\"logstash-2014.09.09\\",
|
||||
\\"_type\\": \\"doc\\",
|
||||
\\"_id\\": \\"id123\\",
|
||||
\\"_score\\": 1,
|
||||
\\"_source\\": {
|
||||
\\"message\\": \\"Lorem ipsum dolor sit amet\\",
|
||||
\\"extension\\": \\"html\\",
|
||||
\\"not_mapped\\": \\"yes\\",
|
||||
\\"bytes\\": 100,
|
||||
\\"objectArray\\": [
|
||||
{
|
||||
\\"foo\\": true
|
||||
}
|
||||
],
|
||||
\\"relatedContent\\": {
|
||||
\\"test\\": 1
|
||||
},
|
||||
\\"scripted\\": 123,
|
||||
\\"_underscore\\": 123
|
||||
}
|
||||
}"
|
||||
>
|
||||
<EuiToolTip
|
||||
delay="regular"
|
||||
onMouseOut={[Function]}
|
||||
position="top"
|
||||
>
|
||||
<span
|
||||
className="euiToolTipAnchor"
|
||||
onKeyUp={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
flush="right"
|
||||
iconType="copyClipboard"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
size="xs"
|
||||
>
|
||||
<button
|
||||
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushRight"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<EuiButtonContent
|
||||
className="euiButtonEmpty__content"
|
||||
iconSide="left"
|
||||
iconSize="s"
|
||||
iconType="copyClipboard"
|
||||
textProps={
|
||||
Object {
|
||||
"className": "euiButtonEmpty__text",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="euiButtonContent euiButtonEmpty__content"
|
||||
>
|
||||
<EuiIcon
|
||||
className="euiButtonContent__icon"
|
||||
color="inherit"
|
||||
size="s"
|
||||
type="copyClipboard"
|
||||
>
|
||||
<span
|
||||
className="euiButtonContent__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="copyClipboard"
|
||||
size="s"
|
||||
/>
|
||||
</EuiIcon>
|
||||
<span
|
||||
className="euiButtonEmpty__text"
|
||||
>
|
||||
Copy to clipboard
|
||||
</span>
|
||||
</span>
|
||||
</EuiButtonContent>
|
||||
</button>
|
||||
</EuiButtonEmpty>
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
</EuiCopy>
|
||||
</div>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<div
|
||||
className="euiFlexItem"
|
||||
>
|
||||
<CodeEditor
|
||||
aria-label="Read only JSON view of an elasticsearch document"
|
||||
editorDidMount={[Function]}
|
||||
languageId="xjson"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Object {
|
||||
"automaticLayout": true,
|
||||
"fontSize": 12,
|
||||
"lineNumbers": "on",
|
||||
"minimap": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"overviewRulerBorder": false,
|
||||
"readOnly": true,
|
||||
"scrollBeyondLastLine": false,
|
||||
"scrollbar": Object {
|
||||
"alwaysConsumeMouseWheel": false,
|
||||
},
|
||||
"wordWrap": "on",
|
||||
"wrappingIndent": "indent",
|
||||
}
|
||||
}
|
||||
value="{
|
||||
\\"_index\\": \\"logstash-2014.09.09\\",
|
||||
\\"_type\\": \\"doc\\",
|
||||
\\"_id\\": \\"id123\\",
|
||||
\\"_score\\": 1,
|
||||
\\"_source\\": {
|
||||
\\"message\\": \\"Lorem ipsum dolor sit amet\\",
|
||||
\\"extension\\": \\"html\\",
|
||||
\\"not_mapped\\": \\"yes\\",
|
||||
\\"bytes\\": 100,
|
||||
\\"objectArray\\": [
|
||||
{
|
||||
\\"foo\\": true
|
||||
}
|
||||
],
|
||||
\\"relatedContent\\": {
|
||||
\\"test\\": 1
|
||||
},
|
||||
\\"scripted\\": 123,
|
||||
\\"_underscore\\": 123
|
||||
}
|
||||
}"
|
||||
width={123}
|
||||
>
|
||||
<EuiErrorBoundary>
|
||||
<Suspense
|
||||
fallback={<Fallback />}
|
||||
>
|
||||
<Fallback>
|
||||
<EuiDelayRender
|
||||
delay={500}
|
||||
/>
|
||||
</Fallback>
|
||||
</Suspense>
|
||||
</EuiErrorBoundary>
|
||||
</CodeEditor>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</JsonCodeEditorCommon>
|
||||
</Memo()>
|
||||
</SourceViewer>
|
||||
`;
|
||||
|
||||
exports[`Source Viewer component renders loading state 1`] = `
|
||||
<SourceViewer
|
||||
hasLineNumbers={true}
|
||||
id="1"
|
||||
index="index1"
|
||||
indexPatternId="xyz"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {
|
||||
"date": Object {
|
||||
"full": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"long": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"medium": Object {
|
||||
"day": "numeric",
|
||||
"month": "short",
|
||||
"year": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"day": "numeric",
|
||||
"month": "numeric",
|
||||
"year": "2-digit",
|
||||
},
|
||||
},
|
||||
"number": Object {
|
||||
"currency": Object {
|
||||
"style": "currency",
|
||||
},
|
||||
"percent": Object {
|
||||
"style": "percent",
|
||||
},
|
||||
},
|
||||
"relative": Object {
|
||||
"days": Object {
|
||||
"units": "day",
|
||||
},
|
||||
"hours": Object {
|
||||
"units": "hour",
|
||||
},
|
||||
"minutes": Object {
|
||||
"units": "minute",
|
||||
},
|
||||
"months": Object {
|
||||
"units": "month",
|
||||
},
|
||||
"seconds": Object {
|
||||
"units": "second",
|
||||
},
|
||||
"years": Object {
|
||||
"units": "year",
|
||||
},
|
||||
},
|
||||
"time": Object {
|
||||
"full": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"long": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"medium": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
width={123}
|
||||
>
|
||||
<div
|
||||
className="sourceViewer__loading"
|
||||
>
|
||||
<EuiLoadingSpinner
|
||||
className="sourceViewer__loadingSpinner"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingSpinner euiLoadingSpinner--medium sourceViewer__loadingSpinner"
|
||||
/>
|
||||
</EuiLoadingSpinner>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
>
|
||||
<div
|
||||
className="euiText euiText--extraSmall"
|
||||
>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
component="div"
|
||||
>
|
||||
<div
|
||||
className="euiTextColor euiTextColor--subdued"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Loading JSON"
|
||||
id="discover.loadingJSON"
|
||||
values={Object {}}
|
||||
>
|
||||
Loading JSON
|
||||
</FormattedMessage>
|
||||
</div>
|
||||
</EuiTextColor>
|
||||
</div>
|
||||
</EuiText>
|
||||
</div>
|
||||
</SourceViewer>
|
||||
`;
|
|
@ -0,0 +1,14 @@
|
|||
.sourceViewer__loading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
flex: 1 0 100%;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: $euiSizeS;
|
||||
}
|
||||
|
||||
.sourceViewer__loadingSpinner {
|
||||
margin-right: $euiSizeS;
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { SourceViewer } from './source_viewer';
|
||||
import * as hooks from '../doc/use_es_doc_search';
|
||||
import * as useUiSettingHook from 'src/plugins/kibana_react/public/ui_settings/use_ui_setting';
|
||||
import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { JsonCodeEditorCommon } from '../json_code_editor/json_code_editor_common';
|
||||
|
||||
jest.mock('../../../kibana_services', () => ({
|
||||
getServices: jest.fn(),
|
||||
}));
|
||||
|
||||
import { getServices, IndexPattern } from '../../../kibana_services';
|
||||
|
||||
const mockIndexPattern = {
|
||||
getComputedFields: () => [],
|
||||
} as never;
|
||||
const getMock = jest.fn(() => Promise.resolve(mockIndexPattern));
|
||||
const mockIndexPatternService = ({
|
||||
get: getMock,
|
||||
} as unknown) as IndexPattern;
|
||||
|
||||
(getServices as jest.Mock).mockImplementation(() => ({
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
if (key === 'discover:useNewFieldsApi') {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
data: {
|
||||
indexPatternService: mockIndexPatternService,
|
||||
},
|
||||
}));
|
||||
describe('Source Viewer component', () => {
|
||||
test('renders loading state', () => {
|
||||
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, null, () => {}]);
|
||||
|
||||
const comp = mountWithIntl(
|
||||
<SourceViewer
|
||||
id={'1'}
|
||||
index={'index1'}
|
||||
indexPatternId={'xyz'}
|
||||
width={123}
|
||||
hasLineNumbers={true}
|
||||
/>
|
||||
);
|
||||
expect(comp).toMatchSnapshot();
|
||||
const loadingIndicator = comp.find(EuiLoadingSpinner);
|
||||
expect(loadingIndicator).not.toBe(null);
|
||||
});
|
||||
|
||||
test('renders error state', () => {
|
||||
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, null, () => {}]);
|
||||
|
||||
const comp = mountWithIntl(
|
||||
<SourceViewer
|
||||
id={'1'}
|
||||
index={'index1'}
|
||||
indexPatternId={'xyz'}
|
||||
width={123}
|
||||
hasLineNumbers={true}
|
||||
/>
|
||||
);
|
||||
expect(comp).toMatchSnapshot();
|
||||
const errorPrompt = comp.find(EuiEmptyPrompt);
|
||||
expect(errorPrompt.length).toBe(1);
|
||||
const refreshButton = comp.find(EuiButton);
|
||||
expect(refreshButton.length).toBe(1);
|
||||
});
|
||||
|
||||
test('renders json code editor', () => {
|
||||
const mockHit = {
|
||||
_index: 'logstash-2014.09.09',
|
||||
_type: 'doc',
|
||||
_id: 'id123',
|
||||
_score: 1,
|
||||
_source: {
|
||||
message: 'Lorem ipsum dolor sit amet',
|
||||
extension: 'html',
|
||||
not_mapped: 'yes',
|
||||
bytes: 100,
|
||||
objectArray: [{ foo: true }],
|
||||
relatedContent: {
|
||||
test: 1,
|
||||
},
|
||||
scripted: 123,
|
||||
_underscore: 123,
|
||||
},
|
||||
} as never;
|
||||
jest
|
||||
.spyOn(hooks, 'useEsDocSearch')
|
||||
.mockImplementation(() => [2, mockHit, mockIndexPattern, () => {}]);
|
||||
jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => {
|
||||
return false;
|
||||
});
|
||||
const comp = mountWithIntl(
|
||||
<SourceViewer
|
||||
id={'1'}
|
||||
index={'index1'}
|
||||
indexPatternId={'xyz'}
|
||||
width={123}
|
||||
hasLineNumbers={true}
|
||||
/>
|
||||
);
|
||||
expect(comp).toMatchSnapshot();
|
||||
const jsonCodeEditor = comp.find(JsonCodeEditorCommon);
|
||||
expect(jsonCodeEditor).not.toBe(null);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 './source_viewer.scss';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useEsDocSearch } from '../doc/use_es_doc_search';
|
||||
import { JSONCodeEditorCommonMemoized } from '../json_code_editor/json_code_editor_common';
|
||||
import { ElasticRequestState } from '../doc/elastic_request_state';
|
||||
import { getServices } from '../../../../public/kibana_services';
|
||||
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
|
||||
|
||||
interface SourceViewerProps {
|
||||
id: string;
|
||||
index: string;
|
||||
indexPatternId: string;
|
||||
hasLineNumbers: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const SourceViewer = ({
|
||||
id,
|
||||
index,
|
||||
indexPatternId,
|
||||
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({
|
||||
id,
|
||||
index,
|
||||
indexPatternId,
|
||||
indexPatternService,
|
||||
requestSource: useNewFieldsApi,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (reqState === ElasticRequestState.Found && hit) {
|
||||
setJsonValue(JSON.stringify(hit, undefined, 2));
|
||||
}
|
||||
}, [reqState, hit]);
|
||||
|
||||
// setting editor height based on lines height and count to stretch and fit its content
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const editorElement = editor.getDomNode();
|
||||
|
||||
if (!editorElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
|
||||
const lineCount = editor.getModel()?.getLineCount() || 1;
|
||||
const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight;
|
||||
if (!jsonValue || jsonValue === '') {
|
||||
editorElement.style.height = '0px';
|
||||
} else {
|
||||
editorElement.style.height = `${height}px`;
|
||||
}
|
||||
editor.layout();
|
||||
}, [editor, jsonValue]);
|
||||
|
||||
const loadingState = (
|
||||
<div className="sourceViewer__loading">
|
||||
<EuiLoadingSpinner className="sourceViewer__loadingSpinner" />
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage id="discover.loadingJSON" defaultMessage="Loading JSON" />
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
|
||||
const errorMessageTitle = (
|
||||
<h2>
|
||||
{i18n.translate('discover.sourceViewer.errorMessageTitle', {
|
||||
defaultMessage: 'An Error Occurred',
|
||||
})}
|
||||
</h2>
|
||||
);
|
||||
const errorMessage = (
|
||||
<div>
|
||||
{i18n.translate('discover.sourceViewer.errorMessage', {
|
||||
defaultMessage: 'Could not fetch data at this time. Refresh the tab to try again.',
|
||||
})}
|
||||
<EuiSpacer size="s" />
|
||||
<EuiButton iconType="refresh" onClick={requestData}>
|
||||
{i18n.translate('discover.sourceViewer.refresh', {
|
||||
defaultMessage: 'Refresh',
|
||||
})}
|
||||
</EuiButton>
|
||||
</div>
|
||||
);
|
||||
const errorState = (
|
||||
<EuiEmptyPrompt iconType="alert" title={errorMessageTitle} body={errorMessage} />
|
||||
);
|
||||
|
||||
if (
|
||||
reqState === ElasticRequestState.Error ||
|
||||
reqState === ElasticRequestState.NotFound ||
|
||||
reqState === ElasticRequestState.NotFoundIndexPattern
|
||||
) {
|
||||
return errorState;
|
||||
}
|
||||
|
||||
if (reqState === ElasticRequestState.Loading || jsonValue === '') {
|
||||
return loadingState;
|
||||
}
|
||||
|
||||
return (
|
||||
<JSONCodeEditorCommonMemoized
|
||||
jsonValue={jsonValue}
|
||||
width={width}
|
||||
hasLineNumbers={hasLineNumbers}
|
||||
onEditorDidMount={(editorNode: monaco.editor.IStandaloneCodeEditor) => setEditor(editorNode)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -37,7 +37,7 @@ import { UrlGeneratorState } from '../../share/public';
|
|||
import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types';
|
||||
import { DocViewsRegistry } from './application/doc_views/doc_views_registry';
|
||||
import { DocViewTable } from './application/components/table/table';
|
||||
import { JsonCodeEditor } from './application/components/json_code_editor/json_code_editor';
|
||||
|
||||
import {
|
||||
setDocViewsRegistry,
|
||||
setUrlTracker,
|
||||
|
@ -63,6 +63,7 @@ import { SearchEmbeddableFactory } from './application/embeddable';
|
|||
import { UsageCollectionSetup } from '../../usage_collection/public';
|
||||
import { replaceUrlHashQuery } from '../../kibana_utils/public/';
|
||||
import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public';
|
||||
import { SourceViewer } from './application/components/source_viewer/source_viewer';
|
||||
|
||||
declare module '../../share/public' {
|
||||
export interface UrlGeneratorStateMapping {
|
||||
|
@ -178,7 +179,6 @@ export class DiscoverPlugin
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.docViewsRegistry = new DocViewsRegistry();
|
||||
setDocViewsRegistry(this.docViewsRegistry);
|
||||
this.docViewsRegistry.addDocView({
|
||||
|
@ -193,8 +193,14 @@ export class DiscoverPlugin
|
|||
defaultMessage: 'JSON',
|
||||
}),
|
||||
order: 20,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
component: ({ hit }) => <JsonCodeEditor json={hit as any} hasLineNumbers />,
|
||||
component: ({ hit, indexPattern }) => (
|
||||
<SourceViewer
|
||||
index={hit._index}
|
||||
id={hit._id}
|
||||
indexPatternId={indexPattern?.id || ''}
|
||||
hasLineNumbers
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
const {
|
||||
|
@ -273,6 +279,7 @@ export class DiscoverPlugin
|
|||
|
||||
// make sure the index pattern list is up to date
|
||||
await dataStart.indexPatterns.clearCache();
|
||||
|
||||
const { renderApp } = await import('./application/application');
|
||||
params.element.classList.add('dscAppWrapper');
|
||||
const unmount = await renderApp(innerAngularName, params.element);
|
||||
|
|
|
@ -11,6 +11,7 @@ import { FtrProviderContext } from './ftr_provider_context';
|
|||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const docTable = getService('docTable');
|
||||
const retry = getService('retry');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
@ -58,5 +59,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await PageObjects.discover.getDocHeader()).not.to.have.string('_score');
|
||||
expect(await PageObjects.discover.getDocHeader()).to.have.string('Document');
|
||||
});
|
||||
|
||||
it('displays _source viewer in doc viewer', async function () {
|
||||
await docTable.clickRowToggle({ rowIndex: 0 });
|
||||
|
||||
await PageObjects.discover.isShowingDocViewer();
|
||||
await PageObjects.discover.clickDocViewerTab(1);
|
||||
await PageObjects.discover.expectSourceViewerToExist();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -296,6 +296,14 @@ export class DiscoverPageObject extends FtrService {
|
|||
return await this.testSubjects.exists('kbnDocViewer');
|
||||
}
|
||||
|
||||
public async clickDocViewerTab(index: number) {
|
||||
return await this.find.clickByCssSelector(`#kbn_doc_viewer_tab_${index}`);
|
||||
}
|
||||
|
||||
public async expectSourceViewerToExist() {
|
||||
return await this.find.byClassName('monaco-editor');
|
||||
}
|
||||
|
||||
public async getMarks() {
|
||||
const table = await this.docTable.getTable();
|
||||
const marks = await table.findAllByTagName('mark');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue