mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
parent
88b15ff267
commit
fcb8cf4233
13 changed files with 415 additions and 15 deletions
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 = (
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue