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:
CJ Cenizal 2020-09-25 08:25:49 -07:00 committed by GitHub
parent a88c27258e
commit 0dc89cb716
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 551 additions and 73 deletions

View file

@ -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 = ({

View file

@ -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];

View file

@ -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>
);
};

View file

@ -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 />
</>
)}

View file

@ -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>
);
};

View file

@ -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 fields index.',
})}
</p>
</EuiCallOut>
}
)}
/>
</>
)}
</>

View file

@ -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>
);
}}

View file

@ -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 =>

View file

@ -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 />
</>
);
};

View file

@ -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>
</>
);
}

View file

@ -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}

View file

@ -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} />;
};

View file

@ -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,
};

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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>

View file

@ -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',

View file

@ -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;

View file

@ -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,

View file

@ -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';

View file

@ -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');
});
});
});

View file

@ -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,

View file

@ -51,3 +51,5 @@ export {
OnJsonEditorUpdateHandler,
GlobalFlyout,
} from '../../../../../../../src/plugins/es_ui_shared/public';
export { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public';

View file

@ -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'> };

View file

@ -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
);

View file

@ -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 });

View file

@ -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';