[Mappings editor] Add missing relations parameter (#55003) (#55527)

This commit is contained in:
Sébastien Loix 2020-01-22 19:22:43 +05:30 committed by GitHub
parent 88b15ff267
commit fcb8cf4233
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 415 additions and 15 deletions

View file

@ -17,13 +17,14 @@
* under the License.
*/
import { useState, useRef } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useFormContext } from '../form_context';
interface Props {
path: string;
initialNumberOfItems?: number;
readDefaultValueOnForm?: boolean;
children: (args: {
items: ArrayItem[];
addItem: () => void;
@ -52,9 +53,15 @@ export interface ArrayItem {
*
* Look at the README.md for some examples.
*/
export const UseArray = ({ path, initialNumberOfItems, children }: Props) => {
export const UseArray = ({
path,
initialNumberOfItems,
readDefaultValueOnForm = true,
children,
}: Props) => {
const didMountRef = useRef(false);
const form = useFormContext();
const defaultValues = form.getFieldDefaultValue(path) as any[];
const defaultValues = readDefaultValueOnForm && (form.getFieldDefaultValue(path) as any[]);
const uniqueId = useRef(0);
const getInitialItemsFromValues = (values: any[]): ArrayItem[] =>
@ -99,5 +106,13 @@ export const UseArray = ({ path, initialNumberOfItems, children }: Props) => {
});
};
useEffect(() => {
if (didMountRef.current) {
setItems(updatePaths(items));
} else {
didMountRef.current = true;
}
}, [path]);
return children({ items, addItem, removeItem });
};

View file

@ -29,17 +29,27 @@ export interface Props {
defaultValue?: unknown;
component?: FunctionComponent<any> | 'input';
componentProps?: Record<string, any>;
readDefaultValueOnForm?: boolean;
onChange?: (value: unknown) => void;
children?: (field: FieldHook) => JSX.Element;
}
export const UseField = React.memo(
({ path, config, defaultValue, component, componentProps, onChange, children }: Props) => {
({
path,
config,
defaultValue,
component,
componentProps,
readDefaultValueOnForm = true,
onChange,
children,
}: Props) => {
const form = useFormContext();
component = component === undefined ? 'input' : component;
componentProps = componentProps === undefined ? {} : componentProps;
if (typeof defaultValue === 'undefined') {
if (typeof defaultValue === 'undefined' && readDefaultValueOnForm) {
defaultValue = form.getFieldDefaultValue(path);
}

View file

@ -198,6 +198,12 @@ export function useForm<T extends FormData = FormData>(
});
formData$.current.next(currentFormData as T);
/**
* After removing a field, the form validity might have changed
* (an invalid field might have been removed and now the form is valid)
*/
updateFormValidity();
};
const setFieldValue: FormHook<T>['setFieldValue'] = (fieldName, value) => {

View file

@ -8,21 +8,31 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { ParameterName } from '../../../types';
import { EditFieldFormRow } from '../fields/edit_field';
import { documentationService } from '../../../../../services/documentation';
export const EagerGlobalOrdinalsParameter = () => (
interface Props {
configPath?: ParameterName;
description?: string | JSX.Element;
}
export const EagerGlobalOrdinalsParameter = ({
description,
configPath = 'eager_global_ordinals',
}: Props) => (
<EditFieldFormRow
title={i18n.translate('xpack.idxMgmt.mappingsEditor.eagerGlobalOrdinalsFieldTitle', {
defaultMessage: 'Build global ordinals at index time',
})}
description={i18n.translate(
'xpack.idxMgmt.mappingsEditor.eagerGlobalOrdinalsFieldDescription',
{
defaultMessage:
'By default, global ordinals are built at search time, which optimizes for index speed. You can optimize for search performance by building them at index time instead.',
}
)}
description={
description
? description
: i18n.translate('xpack.idxMgmt.mappingsEditor.eagerGlobalOrdinalsFieldDescription', {
defaultMessage:
'By default, global ordinals are built at search time, which optimizes for index speed. You can optimize for search performance by building them at index time instead.',
})
}
docLink={{
text: i18n.translate('xpack.idxMgmt.mappingsEditor.eagerGlobalOrdinalsDocLinkText', {
defaultMessage: 'Global ordinals documentation',
@ -30,5 +40,6 @@ export const EagerGlobalOrdinalsParameter = () => (
href: documentationService.getEagerGlobalOrdinalsLink(),
}}
formFieldPath="eager_global_ordinals"
configPath={configPath}
/>
);

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { relationsSerializer, relationsDeserializer } from './relations_parameter';
export * from './name_parameter';
export * from './index_parameter';
@ -53,3 +55,9 @@ export * from './split_queries_on_whitespace_parameter';
export * from './locale_parameter';
export * from './max_shingle_size_parameter';
export * from './relations_parameter';
export const PARAMETER_SERIALIZERS = [relationsSerializer];
export const PARAMETER_DESERIALIZERS = [relationsDeserializer];

View file

@ -0,0 +1,268 @@
/*
* 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 {
EuiButtonEmpty,
EuiToolTip,
EuiButtonIcon,
EuiSpacer,
EuiCallOut,
EuiLink,
EuiBasicTable,
EuiBasicTableColumn,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
UseField,
UseArray,
ArrayItem,
FieldConfig,
TextField,
ComboBoxField,
} from '../../../shared_imports';
import { Field } from '../../../types';
import { documentationService } from '../../../../../services/documentation';
import { EditFieldFormRow } from '../fields/edit_field';
// This is the Elasticsearch interface to declare relations
interface RelationsES {
[parent: string]: string | string[];
}
// Internally we will use this type for "relations" as it is more UI friendly
// to loop over the relations and its children
type RelationsInternal = Array<{ parent: string; children: string[] }>;
/**
* Export custom serializer to be used when we need to serialize the form data to be sent to ES
* @param field The field to be serialized
*/
export const relationsSerializer = (field: Field): Field => {
if (field.relations === undefined) {
return field;
}
const relations = field.relations as RelationsInternal;
const relationsSerialized = relations.reduce(
(acc, item) => ({
...acc,
[item.parent]: item.children.length === 1 ? item.children[0] : item.children,
}),
{} as RelationsES
);
return {
...field,
relations: relationsSerialized,
};
};
/**
* Export custom deserializer to be used when we need to deserialize the data coming from ES
* @param field The field to be serialized
*/
export const relationsDeserializer = (field: Field): Field => {
if (field.relations === undefined) {
return field;
}
const relations = field.relations as RelationsES;
const relationsDeserialized = Object.entries(relations).map(([parent, children]) => ({
parent,
children: typeof children === 'string' ? [children] : children,
}));
return {
...field,
relations: relationsDeserialized,
};
};
const childConfig: FieldConfig = {
defaultValue: [],
};
export const RelationsParameter = () => {
const renderWarning = () => (
<EuiCallOut
color="warning"
iconType="alert"
size="s"
title={
<FormattedMessage
id="xpack.idxMgmt.mappingsEditor.join.multiLevelsParentJoinWarningTitle"
defaultMessage="Avoid using multiple levels to replicate a relational model. Each relation level increases computation time and memory consumption at query time. For best performance, {docsLink}"
values={{
docsLink: (
<EuiLink
href={documentationService.getJoinMultiLevelsPerformanceLink()}
target="_blank"
>
{i18n.translate(
'xpack.idxMgmt.mappingsEditor.join.multiLevelsPerformanceDocumentationLink',
{
defaultMessage: 'denormalize your data.',
}
)}
</EuiLink>
),
}}
/>
}
/>
);
return (
<EditFieldFormRow
title={i18n.translate('xpack.idxMgmt.mappingsEditor.relationshipsTitle', {
defaultMessage: 'Relationships',
})}
withToggle={false}
>
<UseArray path="relations" initialNumberOfItems={0}>
{({ items, addItem, removeItem }) => {
const columns: Array<EuiBasicTableColumn<any>> = [
// Parent column
{
name: i18n.translate(
'xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.parentColumnTitle',
{
defaultMessage: 'Parent',
}
),
render: (item: ArrayItem) => {
// By adding ".parent" to the path, we are saying that we want an **object**
// to be created for each array item.
// This object will have a "parent" property with the field value.
return (
<div style={{ width: '100%' }}>
<UseField
path={`${item.path}.parent`}
component={TextField}
componentProps={{
euiFieldProps: {
'aria-label': i18n.translate(
'xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.parentFieldAriaLabel',
{
defaultMessage: 'Parent field',
}
),
},
}}
// For a newly created relation, we don't want to read
// its default value provided to the form because... it is new! :)
readDefaultValueOnForm={!item.isNew}
/>
</div>
);
},
},
// Children column (ComboBox)
{
name: i18n.translate(
'xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.childrenColumnTitle',
{
defaultMessage: 'Children',
}
),
render: (item: ArrayItem) => {
return (
<div style={{ width: '100%' }}>
<UseField
path={`${item.path}.children`}
config={childConfig}
component={ComboBoxField}
componentProps={{
euiFieldProps: {
'aria-label': i18n.translate(
'xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.childrenFieldAriaLabel',
{
defaultMessage: 'Children field',
}
),
},
}}
readDefaultValueOnForm={!item.isNew}
/>
</div>
);
},
},
// Actions column
{
width: '48px',
actions: [
{
render: ({ id }: ArrayItem) => {
const label = i18n.translate(
'xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.removeRelationshipTooltipLabel',
{
defaultMessage: 'Remove relationship',
}
);
return (
<EuiToolTip content={label} delay="long">
<EuiButtonIcon
data-test-subj="removeRelationshipButton"
aria-label={label}
iconType="minusInCircle"
color="danger"
onClick={() => removeItem(id)}
/>
</EuiToolTip>
);
},
},
],
},
];
return (
<>
{items.length > 1 && (
<>
{renderWarning()}
<EuiSpacer />
</>
)}
<EuiBasicTable
items={items}
itemId="id"
columns={columns}
noItemsMessage={i18n.translate(
'xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.emptyTableMessage',
{
defaultMessage: 'No relationship defined',
}
)}
hasActions
/>
{/* Add relation button */}
<EuiButtonEmpty
onClick={addItem}
iconType="plusInCircleFilled"
data-test-subj="addRelationButton"
>
{i18n.translate(
'xpack.idxMgmt.mappingsEditor.joinType.addRelationshipButtonLabel',
{
defaultMessage: 'Add relationship',
}
)}
</EuiButtonEmpty>
</>
);
}}
</UseArray>
</EditFieldFormRow>
);
};

View file

@ -24,6 +24,7 @@ import { SearchAsYouType } from './search_as_you_type';
import { FlattenedType } from './flattened_type';
import { ShapeType } from './shape_type';
import { DenseVectorType } from './dense_vector_type';
import { JoinType } from './join_type';
const typeToParametersFormMap: { [key in DataType]?: ComponentType<any> } = {
alias: AliasType,
@ -44,6 +45,7 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType<any> } = {
flattened: FlattenedType,
shape: ShapeType,
dense_vector: DenseVectorType,
join: JoinType,
};
export const getParametersFormForType = (

View file

@ -0,0 +1,37 @@
/*
* 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 { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
import { RelationsParameter, EagerGlobalOrdinalsParameter } from '../../field_parameters';
const i18nTexts = {
eagerGlobalOrdinalsDescription: i18n.translate(
'xpack.idxMgmt.mappingsEditor.join.eagerGlobalOrdinalsFieldDescription',
{
defaultMessage:
'The join field uses global ordinals to speed up joins. By default, if the index has changed, global ordinals for the join field will be rebuilt as part of the refresh. This can add significant time to the refresh, however most of the times this is the right trade-off.',
}
),
};
export const JoinType = () => {
return (
<>
<BasicParametersSection>
<RelationsParameter />
</BasicParametersSection>
<AdvancedParametersSection>
<EagerGlobalOrdinalsParameter
configPath="eager_global_ordinals_join"
description={i18nTexts.eagerGlobalOrdinalsDescription}
/>
</AdvancedParametersSection>
</>
);
};

View file

@ -663,6 +663,11 @@ export const PARAMETERS_DEFINITION = {
},
schema: t.boolean,
},
eager_global_ordinals_join: {
fieldConfig: {
defaultValue: true,
},
},
index_phrases: {
fieldConfig: {
defaultValue: false,
@ -894,6 +899,12 @@ export const PARAMETERS_DEFINITION = {
},
schema: t.string,
},
relations: {
fieldConfig: {
defaultValue: [] as any, // Needed for FieldParams typing
},
schema: t.record(t.string, t.union([t.string, t.array(t.string)])),
},
max_shingle_size: {
fieldConfig: {
type: FIELD_TYPES.SELECT,

View file

@ -5,6 +5,10 @@
*/
import { SerializerFunc } from '../shared_imports';
import {
PARAMETER_SERIALIZERS,
PARAMETER_DESERIALIZERS,
} from '../components/document_fields/field_parameters';
import { Field, DataType, MainType, SubType } from '../types';
import { INDEX_DEFAULT, MAIN_DATA_TYPE_DEFINITION } from '../constants';
import { getMainTypeFromSubType } from './utils';
@ -21,6 +25,25 @@ const sanitizeField = (field: Field): Field =>
{} as any
);
/**
* Run custom parameter serializers on field.
* Each serializer takes the field as single argument and returns it serialized in an immutable way.
* @param field The field we are serializing
*/
const runParametersSerializers = (field: Field): Field =>
PARAMETER_SERIALIZERS.reduce((fieldSerialized, serializer) => serializer(fieldSerialized), field);
/**
* Run custom parameter deserializers on field.
* Each deserializer takes the field as single argument and returns it deserialized in an immutable way.
* @param field The field we are deserializing
*/
const runParametersDeserializers = (field: Field): Field =>
PARAMETER_DESERIALIZERS.reduce(
(fieldDeserialized, serializer) => serializer(fieldDeserialized),
field
);
export const fieldSerializer: SerializerFunc<Field> = (field: Field) => {
// If a subType is present, use it as type for ES
if ({}.hasOwnProperty.call(field, 'subType')) {
@ -31,7 +54,7 @@ export const fieldSerializer: SerializerFunc<Field> = (field: Field) => {
// Delete temp fields
delete (field as any).useSameAnalyzerForSearch;
return sanitizeField(field);
return sanitizeField(runParametersSerializers(field));
};
export const fieldDeserializer: SerializerFunc<Field> = (field: Field): Field => {
@ -50,5 +73,5 @@ export const fieldDeserializer: SerializerFunc<Field> = (field: Field): Field =>
(field as any).useSameAnalyzerForSearch =
{}.hasOwnProperty.call(field, 'search_analyzer') === false;
return field;
return runParametersDeserializers(field);
};

View file

@ -16,6 +16,8 @@ export {
OnFormUpdateArg,
SerializerFunc,
UseField,
UseArray,
ArrayItem,
useForm,
useFormContext,
UseMultiFields,
@ -34,6 +36,7 @@ export {
SuperSelectField,
TextAreaField,
TextField,
ComboBoxField,
ToggleField,
JsonEditorField,
} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/components';

View file

@ -99,6 +99,7 @@ export type ParameterName =
| 'index_options_flattened'
| 'index_options_keyword'
| 'eager_global_ordinals'
| 'eager_global_ordinals_join'
| 'index_prefixes'
| 'index_phrases'
| 'norms'
@ -120,6 +121,7 @@ export type ParameterName =
| 'path'
| 'dims'
| 'depth_limit'
| 'relations'
| 'max_shingle_size';
export interface Parameter {

View file

@ -177,6 +177,10 @@ class DocumentationService {
return `${this.esDocsBase}/index-options.html`;
}
public getJoinMultiLevelsPerformanceLink() {
return `${this.esDocsBase}/parent-join.html#_parent_join_and_performance`;
}
public getWellKnownTextLink() {
return 'http://docs.opengeospatial.org/is/12-063r5/12-063r5.html';
}