Add support for runtime field types to mappings editor. (#77420)
* Add support for runtime field types to mappings editor. * Add tests for getTypeLabelFromField util. * Refine callout appearance in term vector and alias parameters.
This commit is contained in:
parent
a88c27258e
commit
0dc89cb716
|
@ -8,7 +8,7 @@ import React, { createContext, useContext } from 'react';
|
|||
import { ScopedHistory } from 'kibana/public';
|
||||
import { ManagementAppMountParams } from 'src/plugins/management/public';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import { CoreStart } from '../../../../../src/core/public';
|
||||
import { CoreSetup, CoreStart } from '../../../../../src/core/public';
|
||||
|
||||
import { IngestManagerSetup } from '../../../ingest_manager/public';
|
||||
import { IndexMgmtMetricsType } from '../types';
|
||||
|
@ -34,6 +34,7 @@ export interface AppDependencies {
|
|||
};
|
||||
history: ScopedHistory;
|
||||
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
|
||||
uiSettings: CoreSetup['uiSettings'];
|
||||
}
|
||||
|
||||
export const AppContextProvider = ({
|
||||
|
|
|
@ -73,6 +73,10 @@ export * from './meta_parameter';
|
|||
|
||||
export * from './ignore_above_parameter';
|
||||
|
||||
export { RuntimeTypeParameter } from './runtime_type_parameter';
|
||||
|
||||
export { PainlessScriptParameter } from './painless_script_parameter';
|
||||
|
||||
export const PARAMETER_SERIALIZERS = [relationsSerializer, dynamicSerializer];
|
||||
|
||||
export const PARAMETER_DESERIALIZERS = [relationsDeserializer, dynamicDeserializer];
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiDescribedFormGroup } from '@elastic/eui';
|
||||
|
||||
import { CodeEditor, UseField } from '../../../shared_imports';
|
||||
import { getFieldConfig } from '../../../lib';
|
||||
import { EditFieldFormRow } from '../fields/edit_field';
|
||||
|
||||
interface Props {
|
||||
stack?: boolean;
|
||||
}
|
||||
|
||||
export const PainlessScriptParameter = ({ stack }: Props) => {
|
||||
return (
|
||||
<UseField path="script.source" config={getFieldConfig('script')}>
|
||||
{(scriptField) => {
|
||||
const error = scriptField.getErrorsMessages();
|
||||
const isInvalid = error ? Boolean(error.length) : false;
|
||||
|
||||
const field = (
|
||||
<EuiFormRow label={scriptField.label} error={error} isInvalid={isInvalid} fullWidth>
|
||||
<CodeEditor
|
||||
languageId="painless"
|
||||
// 99% width allows the editor to resize horizontally. 100% prevents it from resizing.
|
||||
width="99%"
|
||||
height="400px"
|
||||
value={scriptField.value as string}
|
||||
onChange={scriptField.setValue}
|
||||
options={{
|
||||
fontSize: 12,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'indent',
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
||||
const fieldTitle = i18n.translate('xpack.idxMgmt.mappingsEditor.painlessScript.title', {
|
||||
defaultMessage: 'Emitted value',
|
||||
});
|
||||
|
||||
const fieldDescription = i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.painlessScript.description',
|
||||
{
|
||||
defaultMessage: 'Use emit() to define the value of this runtime field.',
|
||||
}
|
||||
);
|
||||
|
||||
if (stack) {
|
||||
return (
|
||||
<EditFieldFormRow title={fieldTitle} description={fieldDescription} withToggle={false}>
|
||||
{field}
|
||||
</EditFieldFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={<h3>{fieldTitle}</h3>}
|
||||
description={fieldDescription}
|
||||
fullWidth={true}
|
||||
>
|
||||
{field}
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
|
@ -93,17 +93,17 @@ export const PathParameter = ({ field, allFields }: Props) => {
|
|||
<>
|
||||
{!Boolean(suggestedFields.length) && (
|
||||
<>
|
||||
<EuiCallOut color="warning">
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.aliasType.noFieldsAddedWarningMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'You need to add at least one field before creating an alias.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
color="warning"
|
||||
title={i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.aliasType.noFieldsAddedWarningMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'You need to add at least one field before creating an alias.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiDescribedFormGroup,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { UseField } from '../../../shared_imports';
|
||||
import { DataType } from '../../../types';
|
||||
import { getFieldConfig } from '../../../lib';
|
||||
import { RUNTIME_FIELD_OPTIONS, TYPE_DEFINITION } from '../../../constants';
|
||||
import { EditFieldFormRow, FieldDescriptionSection } from '../fields/edit_field';
|
||||
|
||||
interface Props {
|
||||
stack?: boolean;
|
||||
}
|
||||
|
||||
export const RuntimeTypeParameter = ({ stack }: Props) => {
|
||||
return (
|
||||
<UseField path="runtime_type" config={getFieldConfig('runtime_type')}>
|
||||
{(runtimeTypeField) => {
|
||||
const { label, value, setValue } = runtimeTypeField;
|
||||
const typeDefinition =
|
||||
TYPE_DEFINITION[(value as EuiComboBoxOptionOption[])[0]!.value as DataType];
|
||||
|
||||
const field = (
|
||||
<>
|
||||
<EuiFormRow label={label} fullWidth>
|
||||
<EuiComboBox
|
||||
placeholder={i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.runtimeType.placeholderLabel',
|
||||
{
|
||||
defaultMessage: 'Select a type',
|
||||
}
|
||||
)}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={RUNTIME_FIELD_OPTIONS}
|
||||
selectedOptions={value as EuiComboBoxOptionOption[]}
|
||||
onChange={setValue}
|
||||
isClearable={false}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{/* Field description */}
|
||||
{typeDefinition && (
|
||||
<FieldDescriptionSection isMultiField={false}>
|
||||
{typeDefinition.description?.() as JSX.Element}
|
||||
</FieldDescriptionSection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const fieldTitle = i18n.translate('xpack.idxMgmt.mappingsEditor.runtimeType.title', {
|
||||
defaultMessage: 'Emitted type',
|
||||
});
|
||||
|
||||
const fieldDescription = i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.runtimeType.description',
|
||||
{
|
||||
defaultMessage: 'Select the type of value emitted by the runtime field.',
|
||||
}
|
||||
);
|
||||
|
||||
if (stack) {
|
||||
return (
|
||||
<EditFieldFormRow title={fieldTitle} description={fieldDescription} withToggle={false}>
|
||||
{field}
|
||||
</EditFieldFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={<h3>{fieldTitle}</h3>}
|
||||
description={fieldDescription}
|
||||
fullWidth={true}
|
||||
>
|
||||
{field}
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
|
@ -56,14 +56,17 @@ export const TermVectorParameter = ({ field, defaultToggleValue }: Props) => {
|
|||
{formData.term_vector === 'with_positions_offsets' && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut color="warning">
|
||||
<p>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.termVectorFieldWarningMessage', {
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
color="warning"
|
||||
title={i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.termVectorFieldWarningMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Setting "With positions and offsets" will double the size of a field’s index.',
|
||||
})}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -14,15 +14,17 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiOutsideClickDetector,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useForm, Form, FormDataProvider } from '../../../../shared_imports';
|
||||
import { EUI_SIZE } from '../../../../constants';
|
||||
import { EUI_SIZE, TYPE_DEFINITION } from '../../../../constants';
|
||||
import { useDispatch } from '../../../../mappings_state_context';
|
||||
import { fieldSerializer } from '../../../../lib';
|
||||
import { Field, NormalizedFields } from '../../../../types';
|
||||
import { Field, NormalizedFields, MainType } from '../../../../types';
|
||||
import { NameParameter, TypeParameter, SubTypeParameter } from '../../field_parameters';
|
||||
import { getParametersFormForType } from './required_parameters_forms';
|
||||
import { FieldBetaBadge } from '../field_beta_badge';
|
||||
import { getRequiredParametersFormForType } from './required_parameters_forms';
|
||||
|
||||
const formWrapper = (props: any) => <form {...props} />;
|
||||
|
||||
|
@ -195,18 +197,27 @@ export const CreateField = React.memo(function CreateFieldComponent({
|
|||
|
||||
<FormDataProvider pathsToWatch={['type', 'subType']}>
|
||||
{({ type, subType }) => {
|
||||
const ParametersForm = getParametersFormForType(
|
||||
const RequiredParametersForm = getRequiredParametersFormForType(
|
||||
type?.[0].value,
|
||||
subType?.[0].value
|
||||
);
|
||||
|
||||
if (!ParametersForm) {
|
||||
if (!RequiredParametersForm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeDefinition = TYPE_DEFINITION[type?.[0].value as MainType];
|
||||
|
||||
return (
|
||||
<div className="mappingsEditor__createFieldRequiredProps">
|
||||
<ParametersForm key={subType ?? type} allFields={allFields} />
|
||||
{typeDefinition.isBeta ? (
|
||||
<>
|
||||
<FieldBetaBadge />
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<RequiredParametersForm key={subType ?? type} allFields={allFields} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { AliasTypeRequiredParameters } from './alias_type';
|
|||
import { TokenCountTypeRequiredParameters } from './token_count_type';
|
||||
import { ScaledFloatTypeRequiredParameters } from './scaled_float_type';
|
||||
import { DenseVectorRequiredParameters } from './dense_vector_type';
|
||||
import { RuntimeTypeRequiredParameters } from './runtime_type';
|
||||
|
||||
export interface ComponentProps {
|
||||
allFields: NormalizedFields['byId'];
|
||||
|
@ -21,9 +22,10 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType<any> } = {
|
|||
token_count: TokenCountTypeRequiredParameters,
|
||||
scaled_float: ScaledFloatTypeRequiredParameters,
|
||||
dense_vector: DenseVectorRequiredParameters,
|
||||
runtime: RuntimeTypeRequiredParameters,
|
||||
};
|
||||
|
||||
export const getParametersFormForType = (
|
||||
export const getRequiredParametersFormForType = (
|
||||
type: MainType,
|
||||
subType?: SubType
|
||||
): ComponentType<ComponentProps> | undefined =>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { RuntimeTypeParameter, PainlessScriptParameter } from '../../../field_parameters';
|
||||
|
||||
export const RuntimeTypeRequiredParameters = () => {
|
||||
return (
|
||||
<>
|
||||
<RuntimeTypeParameter />
|
||||
<PainlessScriptParameter />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -5,12 +5,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { FormDataProvider } from '../../../../shared_imports';
|
||||
import { MainType, SubType, Field } from '../../../../types';
|
||||
import { MainType, SubType, Field, DataTypeDefinition } from '../../../../types';
|
||||
import { TYPE_DEFINITION } from '../../../../constants';
|
||||
import { NameParameter, TypeParameter, SubTypeParameter } from '../../field_parameters';
|
||||
import { FieldBetaBadge } from '../field_beta_badge';
|
||||
import { FieldDescriptionSection } from './field_description_section';
|
||||
|
||||
interface Props {
|
||||
|
@ -19,6 +20,25 @@ interface Props {
|
|||
isMultiField: boolean;
|
||||
}
|
||||
|
||||
const getTypeDefinition = (type: MainType, subType: SubType): DataTypeDefinition | undefined => {
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeDefinition = TYPE_DEFINITION[type];
|
||||
const hasSubType = typeDefinition.subTypes !== undefined;
|
||||
|
||||
if (hasSubType) {
|
||||
if (subType) {
|
||||
return TYPE_DEFINITION[subType];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return typeDefinition;
|
||||
};
|
||||
|
||||
export const EditFieldHeaderForm = React.memo(
|
||||
({ defaultValue, isRootLevelField, isMultiField }: Props) => {
|
||||
return (
|
||||
|
@ -56,27 +76,29 @@ export const EditFieldHeaderForm = React.memo(
|
|||
</EuiFlexGroup>
|
||||
|
||||
{/* Field description */}
|
||||
<FieldDescriptionSection isMultiField={isMultiField}>
|
||||
<FormDataProvider pathsToWatch={['type', 'subType']}>
|
||||
{({ type, subType }) => {
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
const typeDefinition = TYPE_DEFINITION[type[0].value as MainType];
|
||||
const hasSubType = typeDefinition.subTypes !== undefined;
|
||||
<FormDataProvider pathsToWatch={['type', 'subType']}>
|
||||
{({ type, subType }) => {
|
||||
const typeDefinition = getTypeDefinition(
|
||||
type[0].value as MainType,
|
||||
subType && (subType[0].value as SubType)
|
||||
);
|
||||
const description = (typeDefinition?.description?.() as JSX.Element) ?? null;
|
||||
|
||||
if (hasSubType) {
|
||||
if (subType) {
|
||||
const subTypeDefinition = TYPE_DEFINITION[subType as SubType];
|
||||
return (subTypeDefinition?.description?.() as JSX.Element) ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
return typeDefinition.description?.() as JSX.Element;
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</FieldDescriptionSection>
|
||||
{typeDefinition?.isBeta && <FieldBetaBadge />}
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<FieldDescriptionSection isMultiField={isMultiField}>
|
||||
{description}
|
||||
</FieldDescriptionSection>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface Props {
|
||||
|
@ -19,7 +19,6 @@ export const FieldDescriptionSection = ({ children, isMultiField }: Props) => {
|
|||
|
||||
return (
|
||||
<section>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiText size="s" color="subdued">
|
||||
{children}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBetaBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FieldBetaBadge = () => {
|
||||
const betaText = i18n.translate('xpack.idxMgmt.mappingsEditor.fieldBetaBadgeLabel', {
|
||||
defaultMessage: 'Beta',
|
||||
});
|
||||
|
||||
const tooltipText = i18n.translate('xpack.idxMgmt.mappingsEditor.fieldBetaBadgeTooltip', {
|
||||
defaultMessage: 'This field type is not GA. Please help us by reporting any bugs.',
|
||||
});
|
||||
|
||||
return <EuiBetaBadge label={betaText} tooltipContent={tooltipText} />;
|
||||
};
|
|
@ -31,6 +31,7 @@ import { JoinType } from './join_type';
|
|||
import { HistogramType } from './histogram_type';
|
||||
import { ConstantKeywordType } from './constant_keyword_type';
|
||||
import { RankFeatureType } from './rank_feature_type';
|
||||
import { RuntimeType } from './runtime_type';
|
||||
import { WildcardType } from './wildcard_type';
|
||||
import { PointType } from './point_type';
|
||||
|
||||
|
@ -60,6 +61,7 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType<any> } = {
|
|||
histogram: HistogramType,
|
||||
constant_keyword: ConstantKeywordType,
|
||||
rank_feature: RankFeatureType,
|
||||
runtime: RuntimeType,
|
||||
wildcard: WildcardType,
|
||||
point: PointType,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { RuntimeTypeParameter, PainlessScriptParameter } from '../../field_parameters';
|
||||
import { BasicParametersSection } from '../edit_field';
|
||||
|
||||
export const RuntimeType = () => {
|
||||
return (
|
||||
<BasicParametersSection>
|
||||
<RuntimeTypeParameter stack={true} />
|
||||
<PainlessScriptParameter stack={true} />
|
||||
</BasicParametersSection>
|
||||
);
|
||||
};
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { NormalizedField, NormalizedFields } from '../../../types';
|
||||
import { getTypeLabelFromType } from '../../../lib';
|
||||
import { getTypeLabelFromField } from '../../../lib';
|
||||
import { CHILD_FIELD_INDENT_SIZE, LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER } from '../../../constants';
|
||||
|
||||
import { FieldsList } from './fields_list';
|
||||
|
@ -67,6 +67,7 @@ function FieldListItemComponent(
|
|||
isExpanded,
|
||||
path,
|
||||
} = field;
|
||||
|
||||
// When there aren't any "child" fields (the maxNestedDepth === 0), there is no toggle icon on the left of any field.
|
||||
// For that reason, we need to compensate and substract some indent to left align on the page.
|
||||
const substractIndentAmount = maxNestedDepth === 0 ? CHILD_FIELD_INDENT_SIZE * 0.5 : 0;
|
||||
|
@ -280,10 +281,10 @@ function FieldListItemComponent(
|
|||
? i18n.translate('xpack.idxMgmt.mappingsEditor.multiFieldBadgeLabel', {
|
||||
defaultMessage: '{dataType} multi-field',
|
||||
values: {
|
||||
dataType: getTypeLabelFromType(source.type),
|
||||
dataType: getTypeLabelFromField(source),
|
||||
},
|
||||
})
|
||||
: getTypeLabelFromType(source.type)}
|
||||
: getTypeLabelFromField(source)}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { SearchResult } from '../../../types';
|
||||
import { TYPE_DEFINITION } from '../../../constants';
|
||||
import { useDispatch } from '../../../mappings_state_context';
|
||||
import { getTypeLabelFromType } from '../../../lib';
|
||||
import { getTypeLabelFromField } from '../../../lib';
|
||||
import { DeleteFieldProvider } from '../fields/delete_field_provider';
|
||||
|
||||
interface Props {
|
||||
|
@ -121,7 +121,7 @@ export const SearchResultItem = React.memo(function FieldListItemFlatComponent({
|
|||
dataType: TYPE_DEFINITION[source.type].label,
|
||||
},
|
||||
})
|
||||
: getTypeLabelFromType(source.type)}
|
||||
: getTypeLabelFromField(source)}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -13,6 +13,25 @@ import { documentationService } from '../../../services/documentation';
|
|||
import { MainType, SubType, DataType, DataTypeDefinition } from '../types';
|
||||
|
||||
export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {
|
||||
runtime: {
|
||||
value: 'runtime',
|
||||
isBeta: true,
|
||||
label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.runtimeFieldDescription', {
|
||||
defaultMessage: 'Runtime',
|
||||
}),
|
||||
// TODO: Add this once the page exists.
|
||||
// documentation: {
|
||||
// main: '/runtime_field.html',
|
||||
// },
|
||||
description: () => (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.mappingsEditor.dataType.runtimeFieldLongDescription"
|
||||
defaultMessage="Runtime fields define scripts that calculate field values at runtime."
|
||||
/>
|
||||
</p>
|
||||
),
|
||||
},
|
||||
text: {
|
||||
value: 'text',
|
||||
label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.textDescription', {
|
||||
|
@ -896,6 +915,7 @@ export const MAIN_TYPES: MainType[] = [
|
|||
'range',
|
||||
'rank_feature',
|
||||
'rank_features',
|
||||
'runtime',
|
||||
'search_as_you_type',
|
||||
'shape',
|
||||
'text',
|
||||
|
|
|
@ -18,6 +18,7 @@ export const TYPE_NOT_ALLOWED_MULTIFIELD: DataType[] = [
|
|||
'object',
|
||||
'nested',
|
||||
'alias',
|
||||
'runtime',
|
||||
];
|
||||
|
||||
export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map(
|
||||
|
@ -27,6 +28,35 @@ export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map
|
|||
})
|
||||
) as ComboBoxOption[];
|
||||
|
||||
export const RUNTIME_FIELD_OPTIONS = [
|
||||
{
|
||||
label: 'Keyword',
|
||||
value: 'keyword',
|
||||
},
|
||||
{
|
||||
label: 'Long',
|
||||
value: 'long',
|
||||
},
|
||||
{
|
||||
label: 'Double',
|
||||
value: 'double',
|
||||
},
|
||||
{
|
||||
label: 'Date',
|
||||
value: 'date',
|
||||
},
|
||||
{
|
||||
label: 'IP',
|
||||
value: 'ip',
|
||||
},
|
||||
{
|
||||
label: 'Boolean',
|
||||
value: 'boolean',
|
||||
},
|
||||
] as ComboBoxOption[];
|
||||
|
||||
export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const;
|
||||
|
||||
interface SuperSelectOptionConfig {
|
||||
inputDisplay: string;
|
||||
dropdownDisplay: JSX.Element;
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import {
|
||||
AliasOption,
|
||||
DataType,
|
||||
RuntimeType,
|
||||
ComboBoxOption,
|
||||
ParameterName,
|
||||
ParameterDefinition,
|
||||
|
@ -27,6 +28,7 @@ import {
|
|||
import { documentationService } from '../../../services/documentation';
|
||||
import { INDEX_DEFAULT } from './default_values';
|
||||
import { TYPE_DEFINITION } from './data_types_definition';
|
||||
import { RUNTIME_FIELD_OPTIONS } from './field_options';
|
||||
|
||||
const { toInt } = fieldFormatters;
|
||||
const { emptyField, containsCharsField, numberGreaterThanField, isJsonField } = fieldValidators;
|
||||
|
@ -185,6 +187,52 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio
|
|||
},
|
||||
schema: t.string,
|
||||
},
|
||||
runtime_type: {
|
||||
fieldConfig: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.runtimeTypeLabel', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
defaultValue: 'keyword',
|
||||
deserializer: (fieldType: RuntimeType | undefined) => {
|
||||
if (typeof fieldType === 'string' && Boolean(fieldType)) {
|
||||
const label =
|
||||
RUNTIME_FIELD_OPTIONS.find(({ value }) => value === fieldType)?.label ?? fieldType;
|
||||
return [
|
||||
{
|
||||
label,
|
||||
value: fieldType,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
serializer: (value: ComboBoxOption[]) => (value.length === 0 ? '' : value[0].value),
|
||||
},
|
||||
schema: t.string,
|
||||
},
|
||||
script: {
|
||||
fieldConfig: {
|
||||
defaultValue: '',
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.painlessScriptLabel', {
|
||||
defaultMessage: 'Script',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.parameters.validations.scriptIsRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Script must emit() a value.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
schema: t.string,
|
||||
},
|
||||
store: {
|
||||
fieldConfig: {
|
||||
type: FIELD_TYPES.CHECKBOX,
|
||||
|
|
|
@ -4,7 +4,27 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './utils';
|
||||
export {
|
||||
getUniqueId,
|
||||
getChildFieldsName,
|
||||
getFieldMeta,
|
||||
getTypeLabelFromField,
|
||||
getFieldConfig,
|
||||
getTypeMetaFromSource,
|
||||
normalize,
|
||||
deNormalize,
|
||||
updateFieldsPathAfterFieldNameChange,
|
||||
getAllChildFields,
|
||||
getAllDescendantAliases,
|
||||
getFieldAncestors,
|
||||
filterTypesForMultiField,
|
||||
filterTypesForNonRootFields,
|
||||
getMaxNestedDepth,
|
||||
buildFieldTreeFromIds,
|
||||
shouldDeleteChildFieldsAfterTypeChange,
|
||||
canUseMappingsEditor,
|
||||
stripUndefinedValues,
|
||||
} from './utils';
|
||||
|
||||
export * from './serializers';
|
||||
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('../constants', () => ({ MAIN_DATA_TYPE_DEFINITION: {} }));
|
||||
jest.mock('../constants', () => {
|
||||
const { TYPE_DEFINITION } = jest.requireActual('../constants');
|
||||
return { MAIN_DATA_TYPE_DEFINITION: {}, TYPE_DEFINITION };
|
||||
});
|
||||
|
||||
import { stripUndefinedValues } from './utils';
|
||||
import { stripUndefinedValues, getTypeLabelFromField } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('stripUndefinedValues()', () => {
|
||||
|
@ -53,4 +56,46 @@ describe('utils', () => {
|
|||
expect(stripUndefinedValues(dataIN)).toEqual(dataOUT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTypeLabelFromField()', () => {
|
||||
test('returns an unprocessed label for non-runtime fields', () => {
|
||||
expect(
|
||||
getTypeLabelFromField({
|
||||
name: 'testField',
|
||||
type: 'keyword',
|
||||
})
|
||||
).toBe('Keyword');
|
||||
});
|
||||
|
||||
test(`returns a label prepended with 'Other' for unrecognized fields`, () => {
|
||||
expect(
|
||||
getTypeLabelFromField({
|
||||
name: 'testField',
|
||||
// @ts-ignore
|
||||
type: 'hyperdrive',
|
||||
})
|
||||
).toBe('Other: hyperdrive');
|
||||
});
|
||||
|
||||
test("returns a label prepended with 'Runtime' for runtime fields", () => {
|
||||
expect(
|
||||
getTypeLabelFromField({
|
||||
name: 'testField',
|
||||
type: 'runtime',
|
||||
runtime_type: 'keyword',
|
||||
})
|
||||
).toBe('Runtime Keyword');
|
||||
});
|
||||
|
||||
test("returns a label prepended with 'Runtime Other' for unrecognized runtime fields", () => {
|
||||
expect(
|
||||
getTypeLabelFromField({
|
||||
name: 'testField',
|
||||
type: 'runtime',
|
||||
// @ts-ignore
|
||||
runtime_type: 'hyperdrive',
|
||||
})
|
||||
).toBe('Runtime Other: hyperdrive');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -71,8 +71,23 @@ export const getFieldMeta = (field: Field, isMultiField?: boolean): FieldMeta =>
|
|||
};
|
||||
};
|
||||
|
||||
export const getTypeLabelFromType = (type: DataType) =>
|
||||
TYPE_DEFINITION[type] ? TYPE_DEFINITION[type].label : `${TYPE_DEFINITION.other.label}: ${type}`;
|
||||
const getTypeLabel = (type?: DataType): string => {
|
||||
return type && TYPE_DEFINITION[type]
|
||||
? TYPE_DEFINITION[type].label
|
||||
: `${TYPE_DEFINITION.other.label}: ${type}`;
|
||||
};
|
||||
|
||||
export const getTypeLabelFromField = (field: Field) => {
|
||||
const { type, runtime_type: runtimeType } = field;
|
||||
const typeLabel = getTypeLabel(type);
|
||||
|
||||
if (type === 'runtime') {
|
||||
const runtimeTypeLabel = getTypeLabel(runtimeType);
|
||||
return `${typeLabel} ${runtimeTypeLabel}`;
|
||||
}
|
||||
|
||||
return typeLabel;
|
||||
};
|
||||
|
||||
export const getFieldConfig = <T = unknown>(
|
||||
param: ParameterName,
|
||||
|
|
|
@ -51,3 +51,5 @@ export {
|
|||
OnJsonEditorUpdateHandler,
|
||||
GlobalFlyout,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/public';
|
||||
|
||||
export { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
|
|
@ -8,7 +8,7 @@ import { ReactNode } from 'react';
|
|||
import { GenericObject } from './mappings_editor';
|
||||
|
||||
import { FieldConfig } from '../shared_imports';
|
||||
import { PARAMETERS_DEFINITION } from '../constants';
|
||||
import { PARAMETERS_DEFINITION, RUNTIME_FIELD_TYPES } from '../constants';
|
||||
|
||||
export interface DataTypeDefinition {
|
||||
label: string;
|
||||
|
@ -19,6 +19,7 @@ export interface DataTypeDefinition {
|
|||
};
|
||||
subTypes?: { label: string; types: SubType[] };
|
||||
description?: () => ReactNode;
|
||||
isBeta?: boolean;
|
||||
}
|
||||
|
||||
export interface ParameterDefinition {
|
||||
|
@ -35,6 +36,7 @@ export interface ParameterDefinition {
|
|||
}
|
||||
|
||||
export type MainType =
|
||||
| 'runtime'
|
||||
| 'text'
|
||||
| 'keyword'
|
||||
| 'numeric'
|
||||
|
@ -73,6 +75,8 @@ export type SubType = NumericType | RangeType;
|
|||
|
||||
export type DataType = MainType | SubType;
|
||||
|
||||
export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number];
|
||||
|
||||
export type NumericType =
|
||||
| 'long'
|
||||
| 'integer'
|
||||
|
@ -151,6 +155,8 @@ export type ParameterName =
|
|||
| 'depth_limit'
|
||||
| 'relations'
|
||||
| 'max_shingle_size'
|
||||
| 'runtime_type'
|
||||
| 'script'
|
||||
| 'value'
|
||||
| 'meta';
|
||||
|
||||
|
@ -168,6 +174,7 @@ export interface Fields {
|
|||
interface FieldBasic {
|
||||
name: string;
|
||||
type: DataType;
|
||||
runtime_type?: DataType;
|
||||
subType?: SubType;
|
||||
properties?: { [key: string]: Omit<Field, 'name'> };
|
||||
fields?: { [key: string]: Omit<Field, 'name'> };
|
||||
|
|
|
@ -11,7 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
|
|||
import { CoreStart } from '../../../../../src/core/public';
|
||||
|
||||
import { API_BASE_PATH } from '../../common';
|
||||
import { GlobalFlyout } from '../shared_imports';
|
||||
import { createKibanaReactContext, GlobalFlyout } from '../shared_imports';
|
||||
|
||||
import { AppContextProvider, AppDependencies } from './app_context';
|
||||
import { App } from './app';
|
||||
|
@ -30,7 +30,12 @@ export const renderApp = (
|
|||
|
||||
const { i18n, docLinks, notifications, application } = core;
|
||||
const { Context: I18nContext } = i18n;
|
||||
const { services, history, setBreadcrumbs } = dependencies;
|
||||
const { services, history, setBreadcrumbs, uiSettings } = dependencies;
|
||||
|
||||
// uiSettings is required by the CodeEditor component used to edit runtime field Painless scripts.
|
||||
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({
|
||||
uiSettings,
|
||||
});
|
||||
|
||||
const componentTemplateProviderValues = {
|
||||
httpClient: services.httpService.httpClient,
|
||||
|
@ -44,17 +49,19 @@ export const renderApp = (
|
|||
|
||||
render(
|
||||
<I18nContext>
|
||||
<Provider store={indexManagementStore(services)}>
|
||||
<AppContextProvider value={dependencies}>
|
||||
<MappingsEditorProvider>
|
||||
<ComponentTemplatesProvider value={componentTemplateProviderValues}>
|
||||
<GlobalFlyoutProvider>
|
||||
<App history={history} />
|
||||
</GlobalFlyoutProvider>
|
||||
</ComponentTemplatesProvider>
|
||||
</MappingsEditorProvider>
|
||||
</AppContextProvider>
|
||||
</Provider>
|
||||
<KibanaReactContextProvider>
|
||||
<Provider store={indexManagementStore(services)}>
|
||||
<AppContextProvider value={dependencies}>
|
||||
<MappingsEditorProvider>
|
||||
<ComponentTemplatesProvider value={componentTemplateProviderValues}>
|
||||
<GlobalFlyoutProvider>
|
||||
<App history={history} />
|
||||
</GlobalFlyoutProvider>
|
||||
</ComponentTemplatesProvider>
|
||||
</MappingsEditorProvider>
|
||||
</AppContextProvider>
|
||||
</Provider>
|
||||
</KibanaReactContextProvider>
|
||||
</I18nContext>,
|
||||
elem
|
||||
);
|
||||
|
|
|
@ -41,6 +41,7 @@ export async function mountManagementSection(
|
|||
fatalErrors,
|
||||
application,
|
||||
chrome: { docTitle },
|
||||
uiSettings,
|
||||
} = core;
|
||||
|
||||
docTitle.change(PLUGIN.getI18nName(i18n));
|
||||
|
@ -60,6 +61,7 @@ export async function mountManagementSection(
|
|||
services,
|
||||
history,
|
||||
setBreadcrumbs,
|
||||
uiSettings,
|
||||
};
|
||||
|
||||
const unmountAppCallback = renderApp(element, { core, dependencies: appDependencies });
|
||||
|
|
|
@ -44,4 +44,7 @@ export {
|
|||
|
||||
export { isJSON } from '../../../../src/plugins/es_ui_shared/static/validators/string';
|
||||
|
||||
export { reactRouterNavigate } from '../../../../src/plugins/kibana_react/public';
|
||||
export {
|
||||
createKibanaReactContext,
|
||||
reactRouterNavigate,
|
||||
} from '../../../../src/plugins/kibana_react/public';
|
||||
|
|
Loading…
Reference in a new issue