mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Create a Table view for index mappings (#178360)
Index mappings should be viewable in a human-readable format that is not
JSON. We could probably leverage the existing mappings editor UI that we
use when composing index and component templates to do this. In this PR,
we addressed the following items:
- [x] Show a read-only mapping view
- [x] Add a search bar to search for specific fields
7211e778
-b33b-4b2c-93d8-6b9b7d65956e
---------
Co-authored-by: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com>
This commit is contained in:
parent
ad299dee0f
commit
3a5136ac1e
9 changed files with 390 additions and 182 deletions
|
@ -43,6 +43,9 @@ export interface IndexDetailsPageTestBed extends TestBed {
|
|||
getDocsLinkHref: () => string;
|
||||
isErrorDisplayed: () => boolean;
|
||||
clickErrorReloadButton: () => Promise<void>;
|
||||
getTreeViewContent: () => string;
|
||||
clickToggleViewButton: () => Promise<void>;
|
||||
isSearchBarDisabled: () => boolean;
|
||||
};
|
||||
settings: {
|
||||
getCodeBlockContent: () => string;
|
||||
|
@ -195,6 +198,18 @@ export const setup = async ({
|
|||
});
|
||||
component.update();
|
||||
},
|
||||
getTreeViewContent: () => {
|
||||
return find('@timestampField-fieldName').text();
|
||||
},
|
||||
clickToggleViewButton: async () => {
|
||||
await act(async () => {
|
||||
find('indexDetailsMappingsToggleViewButton').simulate('click');
|
||||
});
|
||||
component.update();
|
||||
},
|
||||
isSearchBarDisabled: () => {
|
||||
return find('DocumentFieldsSearch').prop('disabled');
|
||||
},
|
||||
};
|
||||
|
||||
const settings = {
|
||||
|
|
|
@ -476,6 +476,24 @@ describe('<IndexDetailsPage />', () => {
|
|||
expect(tabContent).toEqual(JSON.stringify(testIndexMappings, null, 2));
|
||||
});
|
||||
|
||||
it('displays the mappings in the table view', async () => {
|
||||
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Mappings);
|
||||
await testBed.actions.mappings.clickToggleViewButton();
|
||||
const tabContent = testBed.actions.mappings.getTreeViewContent();
|
||||
expect(tabContent).toContain('@timestamp');
|
||||
});
|
||||
|
||||
it('search bar is enabled in JSON view', async () => {
|
||||
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Mappings);
|
||||
expect(testBed.actions.mappings.isSearchBarDisabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('search bar is disabled in Tree view', async () => {
|
||||
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Mappings);
|
||||
await testBed.actions.mappings.clickToggleViewButton();
|
||||
expect(testBed.actions.mappings.isSearchBarDisabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('sets the docs link href from the documentation service', async () => {
|
||||
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Mappings);
|
||||
const docsLinkHref = testBed.actions.mappings.getDocsLinkHref();
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiText, EuiLink, EuiFlexGroup, EuiFlexItem, EuiFieldSearch } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { documentationService } from '../../../../services/documentation';
|
||||
import { DocumentFieldsSearch } from './document_fields_search';
|
||||
|
||||
interface Props {
|
||||
searchValue: string;
|
||||
|
@ -37,34 +38,7 @@ export const DocumentFieldsHeader = React.memo(({ searchValue, onSearchChange }:
|
|||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldSearch
|
||||
style={{ minWidth: '350px' }}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.documentFields.searchFieldsPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Search fields',
|
||||
}
|
||||
)}
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
// Temporary fix until EUI fixes the contract
|
||||
// See my comment https://github.com/elastic/eui/pull/2723/files#r366725059
|
||||
if (typeof e === 'string') {
|
||||
onSearchChange(e);
|
||||
} else {
|
||||
onSearchChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.documentFields.searchFieldsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Search mapped fields',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<DocumentFieldsSearch searchValue={searchValue} onSearchChange={onSearchChange} />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiFieldSearch, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface Props {
|
||||
searchValue: string;
|
||||
onSearchChange(value: string): void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const DocumentFieldsSearch = React.memo(
|
||||
({ searchValue, onSearchChange, disabled = false }: Props) => {
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldSearch
|
||||
disabled={disabled}
|
||||
style={{ minWidth: '350px' }}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.documentFields.searchFieldsPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Search fields',
|
||||
}
|
||||
)}
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
// Temporary fix until EUI fixes the contract
|
||||
// See my comment https://github.com/elastic/eui/pull/2723/files#r366725059
|
||||
if (typeof e === 'string') {
|
||||
onSearchChange(e);
|
||||
} else {
|
||||
onSearchChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.documentFields.searchFieldsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Search mapped fields',
|
||||
}
|
||||
)}
|
||||
data-test-subj="DocumentFieldsSearch"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -33,7 +33,7 @@ import { DocLinksStart } from './shared_imports';
|
|||
|
||||
type TabName = 'fields' | 'runtimeFields' | 'advanced' | 'templates';
|
||||
|
||||
interface MappingsEditorParsedMetadata {
|
||||
export interface MappingsEditorParsedMetadata {
|
||||
parsedDefaultValue?: {
|
||||
configuration: MappingsConfiguration;
|
||||
fields: { [key: string]: Field };
|
||||
|
|
|
@ -56,7 +56,7 @@ export interface MappingsFields {
|
|||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type DocumentFieldsStatus = 'idle' | 'editingField' | 'creatingField';
|
||||
export type DocumentFieldsStatus = 'idle' | 'editingField' | 'creatingField' | 'disabled';
|
||||
|
||||
export interface DocumentFieldsState {
|
||||
status: DocumentFieldsStatus;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
DocumentFieldsStatus,
|
||||
Field,
|
||||
Mappings,
|
||||
MappingsConfiguration,
|
||||
|
@ -25,16 +26,17 @@ import {
|
|||
import { useMappingsState, useDispatch } from './mappings_state_context';
|
||||
|
||||
interface Args {
|
||||
onChange: OnUpdateHandler;
|
||||
onChange?: OnUpdateHandler;
|
||||
value?: {
|
||||
templates: MappingsTemplates;
|
||||
configuration: MappingsConfiguration;
|
||||
fields: { [key: string]: Field };
|
||||
runtime: RuntimeFields;
|
||||
};
|
||||
status?: DocumentFieldsStatus;
|
||||
}
|
||||
|
||||
export const useMappingsStateListener = ({ onChange, value }: Args) => {
|
||||
export const useMappingsStateListener = ({ onChange, value, status }: Args) => {
|
||||
const state = useMappingsState();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -46,6 +48,12 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => {
|
|||
[runtimeFields]
|
||||
);
|
||||
|
||||
const calculateStatus = (fieldStatus: string | undefined, rootLevelFields: string | any[]) => {
|
||||
if (fieldStatus) return fieldStatus;
|
||||
|
||||
return rootLevelFields.length === 0 ? 'creatingField' : 'idle';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// If we are creating a new field, but haven't entered any name
|
||||
// it is valid and we can byPass its form validation (that requires a "name" to be defined)
|
||||
|
@ -58,79 +66,81 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => {
|
|||
const bypassFieldFormValidation =
|
||||
state.documentFields.status === 'creatingField' && emptyNameValue;
|
||||
|
||||
onChange({
|
||||
// Output a mappings object from the user's input.
|
||||
getData: () => {
|
||||
// Pull the mappings properties from the current editor
|
||||
const fields =
|
||||
state.documentFields.editor === 'json'
|
||||
? state.fieldsJsonEditor.format()
|
||||
: deNormalize(state.fields);
|
||||
if (onChange) {
|
||||
onChange({
|
||||
// Output a mappings object from the user's input.
|
||||
getData: () => {
|
||||
// Pull the mappings properties from the current editor
|
||||
const fields =
|
||||
state.documentFields.editor === 'json'
|
||||
? state.fieldsJsonEditor.format()
|
||||
: deNormalize(state.fields);
|
||||
|
||||
// Get the runtime fields
|
||||
const runtime = deNormalizeRuntimeFields(state.runtimeFields);
|
||||
// Get the runtime fields
|
||||
const runtime = deNormalizeRuntimeFields(state.runtimeFields);
|
||||
|
||||
const configurationData = state.configuration.data.format();
|
||||
const templatesData = state.templates.data.format();
|
||||
const configurationData = state.configuration.data.format();
|
||||
const templatesData = state.templates.data.format();
|
||||
|
||||
const output = {
|
||||
...stripUndefinedValues({
|
||||
...configurationData,
|
||||
...templatesData,
|
||||
}),
|
||||
};
|
||||
const output = {
|
||||
...stripUndefinedValues({
|
||||
...configurationData,
|
||||
...templatesData,
|
||||
}),
|
||||
};
|
||||
|
||||
// Mapped fields
|
||||
if (fields && Object.keys(fields).length > 0) {
|
||||
output.properties = fields;
|
||||
}
|
||||
// Mapped fields
|
||||
if (fields && Object.keys(fields).length > 0) {
|
||||
output.properties = fields;
|
||||
}
|
||||
|
||||
// Runtime fields
|
||||
if (runtime && Object.keys(runtime).length > 0) {
|
||||
output.runtime = runtime;
|
||||
}
|
||||
// Runtime fields
|
||||
if (runtime && Object.keys(runtime).length > 0) {
|
||||
output.runtime = runtime;
|
||||
}
|
||||
|
||||
return Object.keys(output).length > 0 ? (output as Mappings) : undefined;
|
||||
},
|
||||
validate: async () => {
|
||||
const configurationFormValidator =
|
||||
state.configuration.submitForm !== undefined
|
||||
? new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { isValid } = await state.configuration.submitForm!();
|
||||
resolve(isValid);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
: Promise.resolve(true);
|
||||
return Object.keys(output).length > 0 ? (output as Mappings) : undefined;
|
||||
},
|
||||
validate: async () => {
|
||||
const configurationFormValidator =
|
||||
state.configuration.submitForm !== undefined
|
||||
? new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { isValid } = await state.configuration.submitForm!();
|
||||
resolve(isValid);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
: Promise.resolve(true);
|
||||
|
||||
const templatesFormValidator =
|
||||
state.templates.submitForm !== undefined
|
||||
? new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { isValid } = await state.templates.submitForm!();
|
||||
resolve(isValid);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
: Promise.resolve(true);
|
||||
const templatesFormValidator =
|
||||
state.templates.submitForm !== undefined
|
||||
? new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { isValid } = await state.templates.submitForm!();
|
||||
resolve(isValid);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
: Promise.resolve(true);
|
||||
|
||||
const promisesToValidate = [configurationFormValidator, templatesFormValidator];
|
||||
const promisesToValidate = [configurationFormValidator, templatesFormValidator];
|
||||
|
||||
if (state.fieldForm !== undefined && !bypassFieldFormValidation) {
|
||||
promisesToValidate.push(state.fieldForm.validate());
|
||||
}
|
||||
if (state.fieldForm !== undefined && !bypassFieldFormValidation) {
|
||||
promisesToValidate.push(state.fieldForm.validate());
|
||||
}
|
||||
|
||||
return Promise.all(promisesToValidate).then((validationArray) => {
|
||||
const isValid = validationArray.every(Boolean) && state.fieldsJsonEditor.isValid;
|
||||
dispatch({ type: 'validity:update', value: isValid });
|
||||
return isValid;
|
||||
});
|
||||
},
|
||||
isValid: state.isValid,
|
||||
});
|
||||
return Promise.all(promisesToValidate).then((validationArray) => {
|
||||
const isValid = validationArray.every(Boolean) && state.fieldsJsonEditor.isValid;
|
||||
dispatch({ type: 'validity:update', value: isValid });
|
||||
return isValid;
|
||||
});
|
||||
},
|
||||
isValid: state.isValid,
|
||||
});
|
||||
}
|
||||
}, [state, onChange, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -149,11 +159,11 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => {
|
|||
templates: value.templates,
|
||||
fields: parsedFieldsDefaultValue,
|
||||
documentFields: {
|
||||
status: parsedFieldsDefaultValue.rootLevelFields.length === 0 ? 'creatingField' : 'idle',
|
||||
status: calculateStatus(status, parsedFieldsDefaultValue.rootLevelFields),
|
||||
editor: 'default',
|
||||
},
|
||||
runtimeFields: parsedRuntimeFieldsDefaultValue,
|
||||
},
|
||||
});
|
||||
}, [value, parsedFieldsDefaultValue, dispatch, parsedRuntimeFieldsDefaultValue]);
|
||||
}, [value, parsedFieldsDefaultValue, dispatch, status, parsedRuntimeFieldsDefaultValue]);
|
||||
};
|
||||
|
|
|
@ -84,5 +84,5 @@ export const DetailsPageMappings: FunctionComponent<{ index: Index }> = ({ index
|
|||
);
|
||||
}
|
||||
|
||||
return <DetailsPageMappingsContent index={index} data={stringifiedData} />;
|
||||
return <DetailsPageMappingsContent index={index} data={stringifiedData} jsonData={data} />;
|
||||
};
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -16,104 +16,242 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { FunctionComponent, useCallback, useMemo, useState } from 'react';
|
||||
import { Index } from '../../../../../../common';
|
||||
import { documentationService } from '../../../../services';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
import { DocumentFieldsSearch } from '../../../../components/mappings_editor/components/document_fields/document_fields_search';
|
||||
import { FieldsList } from '../../../../components/mappings_editor/components/document_fields/fields';
|
||||
import { SearchResult } from '../../../../components/mappings_editor/components/document_fields/search_fields';
|
||||
import { extractMappingsDefinition } from '../../../../components/mappings_editor/lib';
|
||||
import { MappingsEditorParsedMetadata } from '../../../../components/mappings_editor/mappings_editor';
|
||||
import {
|
||||
useDispatch,
|
||||
useMappingsState,
|
||||
} from '../../../../components/mappings_editor/mappings_state_context';
|
||||
import { useMappingsStateListener } from '../../../../components/mappings_editor/use_state_listener';
|
||||
import { documentationService } from '../../../../services';
|
||||
|
||||
export const DetailsPageMappingsContent: FunctionComponent<{ index: Index; data: string }> = ({
|
||||
index,
|
||||
data,
|
||||
}) => {
|
||||
export const DetailsPageMappingsContent: FunctionComponent<{
|
||||
index: Index;
|
||||
data: string;
|
||||
jsonData: any;
|
||||
}> = ({ index, data, jsonData }) => {
|
||||
const {
|
||||
services: { extensionsService },
|
||||
core: { getUrlForApp },
|
||||
} = useAppContext();
|
||||
return (
|
||||
// using "rowReverse" to keep docs links on the top of the mappings code block on smaller screen
|
||||
<EuiFlexGroup
|
||||
wrap
|
||||
direction="rowReverse"
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const [isJSONVisible, setIsJSONVisible] = useState(true);
|
||||
const onToggleChange = () => {
|
||||
setIsJSONVisible(!isJSONVisible);
|
||||
};
|
||||
const mappingsDefinition = extractMappingsDefinition(jsonData);
|
||||
|
||||
const { parsedDefaultValue } = useMemo<MappingsEditorParsedMetadata>(() => {
|
||||
if (mappingsDefinition === null) {
|
||||
return { multipleMappingsDeclared: true };
|
||||
}
|
||||
|
||||
const {
|
||||
_source,
|
||||
_meta,
|
||||
_routing,
|
||||
_size,
|
||||
dynamic,
|
||||
properties,
|
||||
runtime,
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
numeric_detection,
|
||||
date_detection,
|
||||
dynamic_date_formats,
|
||||
dynamic_templates,
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
} = mappingsDefinition;
|
||||
|
||||
const parsed = {
|
||||
configuration: {
|
||||
_source,
|
||||
_meta,
|
||||
_routing,
|
||||
_size,
|
||||
dynamic,
|
||||
numeric_detection,
|
||||
date_detection,
|
||||
dynamic_date_formats,
|
||||
},
|
||||
fields: properties,
|
||||
templates: {
|
||||
dynamic_templates,
|
||||
},
|
||||
runtime,
|
||||
};
|
||||
|
||||
return { parsedDefaultValue: parsed, multipleMappingsDeclared: false };
|
||||
}, [mappingsDefinition]);
|
||||
|
||||
useMappingsStateListener({ value: parsedDefaultValue, status: 'disabled' });
|
||||
|
||||
const {
|
||||
fields: { byId, rootLevelFields },
|
||||
search,
|
||||
documentFields,
|
||||
} = useMappingsState();
|
||||
|
||||
const getField = useCallback((fieldId: string) => byId[fieldId], [byId]);
|
||||
const fields = useMemo(() => rootLevelFields.map(getField), [rootLevelFields, getField]);
|
||||
const dispatch = useDispatch();
|
||||
const onSearchChange = useCallback(
|
||||
(value: string) => {
|
||||
dispatch({ type: 'search:update', value });
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const searchTerm = search.term.trim();
|
||||
|
||||
const jsonBlock = (
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
isCopyable
|
||||
data-test-subj="indexDetailsMappingsCodeBlock"
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={1}
|
||||
css={css`
|
||||
min-width: 400px;
|
||||
`}
|
||||
>
|
||||
<EuiPanel grow={false} paddingSize="l">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="iInCircle" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.docsCardTitle"
|
||||
defaultMessage="About index mappings"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.docsCardDescription"
|
||||
defaultMessage="Your documents are made up of a set of fields. Index mappings give each field a type
|
||||
(such as keyword, number, or date) and additional subfields. These index mappings determine the functions
|
||||
available in your relevance tuning and search experience."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiLink
|
||||
data-test-subj="indexDetailsMappingsDocsLink"
|
||||
href={documentationService.getMappingDocumentationLink()}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.docsCardLink"
|
||||
defaultMessage="Learn more about mappings"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiPanel>
|
||||
{extensionsService.indexMappingsContent && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{extensionsService.indexMappingsContent.renderContent({ index, getUrlForApp })}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{data}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
|
||||
<EuiFlexItem
|
||||
grow={3}
|
||||
css={css`
|
||||
min-width: 600px;
|
||||
`}
|
||||
>
|
||||
<EuiPanel>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
isCopyable
|
||||
data-test-subj="indexDetailsMappingsCodeBlock"
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
{data}
|
||||
</EuiCodeBlock>
|
||||
</EuiPanel>
|
||||
const treeViewBlock = (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
{mappingsDefinition === null ? (
|
||||
<EuiEmptyPrompt
|
||||
color="danger"
|
||||
iconType="error"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.invalidMappingKeysErrorMessageTitle"
|
||||
defaultMessage="Unable to load the mapping"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.invalidMappingKeysErrorMessageBody"
|
||||
defaultMessage="The mapping contains invalid keys. Please provide a mapping with valid keys."
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
) : searchTerm !== '' ? (
|
||||
<SearchResult result={search.result} documentFieldsState={documentFields} />
|
||||
) : (
|
||||
<FieldsList fields={fields} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
// using "rowReverse" to keep docs links on the top of the mappings code block on smaller screen
|
||||
<>
|
||||
<EuiFlexGroup style={{ marginBottom: euiTheme.size.l }}>
|
||||
<DocumentFieldsSearch
|
||||
searchValue={search.term}
|
||||
onSearchChange={onSearchChange}
|
||||
disabled={isJSONVisible}
|
||||
/>
|
||||
<EuiButton data-test-subj="indexDetailsMappingsToggleViewButton" onClick={onToggleChange}>
|
||||
{isJSONVisible ? (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.tableView"
|
||||
defaultMessage="List"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage id="xpack.idxMgmt.indexDetails.mappings.json" defaultMessage="JSON" />
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
wrap
|
||||
direction="rowReverse"
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={1}
|
||||
css={css`
|
||||
min-width: 400px;
|
||||
`}
|
||||
>
|
||||
<EuiPanel grow={false} paddingSize="l">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="iInCircle" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.docsCardTitle"
|
||||
defaultMessage="About index mappings"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.docsCardDescription"
|
||||
defaultMessage="Your documents are made up of a set of fields. Index mappings give each field a type
|
||||
(such as keyword, number, or date) and additional subfields. These index mappings determine the functions
|
||||
available in your relevance tuning and search experience."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiLink
|
||||
data-test-subj="indexDetailsMappingsDocsLink"
|
||||
href={documentationService.getMappingDocumentationLink()}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.docsCardLink"
|
||||
defaultMessage="Learn more about mappings"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiPanel>
|
||||
{extensionsService.indexMappingsContent && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{extensionsService.indexMappingsContent.renderContent({ index, getUrlForApp })}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
grow={3}
|
||||
css={css`
|
||||
min-width: 600px;
|
||||
`}
|
||||
>
|
||||
<EuiPanel>{isJSONVisible ? jsonBlock : treeViewBlock}</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue