From e793bb7d151080bc263b4f2333f65a9b37037cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 1 Aug 2019 18:14:50 +0200 Subject: [PATCH 1/2] Form lib & temp page to work (to be reverted) --- .../static/forms/components/field.tsx | 210 ++++++++++ .../static/forms/components/form_row.tsx | 54 +++ .../static/forms/components/index.ts | 21 + .../static/forms/errors/constants.ts | 24 ++ .../static/forms/errors/errors.ts | 55 +++ .../static/forms/errors/index.ts | 21 + .../components/form_data_provider.ts | 65 +++ .../forms/hook_form_lib/components/index.ts | 22 + .../hook_form_lib/components/use_array.ts | 87 ++++ .../hook_form_lib/components/use_field.tsx | 88 ++++ .../static/forms/hook_form_lib/constants.ts | 36 ++ .../static/forms/hook_form_lib/hooks/index.ts | 21 + .../forms/hook_form_lib/hooks/use_field.ts | 385 ++++++++++++++++++ .../forms/hook_form_lib/hooks/use_form.ts | 192 +++++++++ .../static/forms/hook_form_lib/index.ts | 27 ++ .../static/forms/hook_form_lib/lib/index.ts | 21 + .../static/forms/hook_form_lib/lib/subject.ts | 51 +++ .../static/forms/hook_form_lib/lib/utils.ts | 102 +++++ .../static/forms/hook_form_lib/types.ts | 149 +++++++ .../static/forms/lib/de_serializers.ts | 36 ++ .../static/forms/lib/field_formatters.ts | 23 ++ .../lib/field_validators/contains_char.ts | 41 ++ .../forms/lib/field_validators/empty_field.ts | 37 ++ .../forms/lib/field_validators/index.ts | 25 ++ .../forms/lib/field_validators/index_name.ts | 47 +++ .../forms/lib/field_validators/min_length.ts | 34 ++ .../min_selectable_selection.ts | 40 ++ .../static/forms/lib/field_validators/url.ts | 38 ++ .../static/forms/lib/index.ts | 21 + .../static/forms/lib/serializers.ts | 49 +++ .../static/validators/array/has_max_length.ts | 20 + .../static/validators/array/has_min_length.ts | 20 + .../static/validators/array/index.ts | 22 + .../static/validators/array/is_empty.ts | 20 + .../validators/string/contains_chars.ts | 32 ++ .../static/validators/string/ends_with.ts | 20 + .../validators/string/has_max_length.ts | 20 + .../validators/string/has_min_length.ts | 20 + .../static/validators/string/index.ts | 26 ++ .../static/validators/string/is_empty.ts | 20 + .../static/validators/string/is_url.ts | 47 +++ .../static/validators/string/starts_with.ts | 20 + .../plugins/index_management/public/app.js | 2 + .../sections/create_index/create_index.tsx | 113 +++++ .../public/sections/create_index/index.ts | 7 + 45 files changed, 2431 insertions(+) create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts create mode 100644 x-pack/legacy/plugins/index_management/public/sections/create_index/create_index.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx b/src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx new file mode 100644 index 0000000000000..01ee046757dcc --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx @@ -0,0 +1,210 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { + EuiFormRow, + EuiFieldText, + EuiFieldNumber, + EuiSelect, + EuiComboBox, + EuiSwitch, + EuiCheckbox, + EuiSelectable, + EuiPanel, + EuiComboBoxOptionProps, +} from '@elastic/eui'; +import uuid from 'uuid'; + +import { + Field as FieldType, + FIELD_TYPES, + VALIDATION_TYPES, + FieldValidateResponse, +} from '../hook_form_lib'; + +interface Props { + field: FieldType; + fieldProps?: Record; +} + +export const Field = ({ field, fieldProps = {} }: Props) => { + let isInvalid: boolean; + let errorMessage: string | null; + + if (field.type === FIELD_TYPES.COMBO_BOX) { + // Errors for the comboBox value (the "array") + const errorMessageField = field.form.isSubmitted ? field.getErrorsMessages() : null; + + // Errors for comboBox option added (the array "item") + const errorMessageArrayItem = field.getErrorsMessages({ + validationType: VALIDATION_TYPES.ARRAY_ITEM, + }); + + isInvalid = field.errors.length + ? field.form.isSubmitted || errorMessageArrayItem !== null + : false; + + // Concatenate error messages. + errorMessage = + errorMessageField && errorMessageArrayItem + ? `${errorMessageField}, ${errorMessageArrayItem}` + : errorMessageField + ? errorMessageField + : errorMessageArrayItem; + } else { + isInvalid = !field.isUpdating && (field.form.isSubmitted && field.errors.length > 0); + errorMessage = + !field.isUpdating && field.errors.length ? (field.errors[0].message as string) : null; + } + + const onCreateComboOption = async (value: string) => { + // Note: for now, we assume that all validations for a comboBox array item are synchronous + // If there is a need to support asynchronous validation, we'll work on it. + const { isValid } = field.validate({ + value, + validationType: VALIDATION_TYPES.ARRAY_ITEM, + }) as FieldValidateResponse; + + if (!isValid) { + field.setValue(field.value as string[]); + return; + } + + const newValue = [...(field.value as string[]), value]; + + field.setValue(newValue); + }; + + const onComboChange = (options: EuiComboBoxOptionProps[]) => { + field.setValue(options.map(option => option.label)); + }; + + const onSearchComboChange = (value: string) => { + if (value) { + field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM); + } + }; + + const doDisplayLabelOnTop = + field.type !== FIELD_TYPES.TOGGLE && field.type !== FIELD_TYPES.CHECKBOX; + + const renderField = () => { + switch (field.type) { + case FIELD_TYPES.NUMBER: + return ( + + ); + case FIELD_TYPES.SELECT: + return ( + { + field.setValue(e.target.value); + }} + hasNoInitialSelection={true} + isInvalid={false} + {...(fieldProps as { options: any; [key: string]: any })} + /> + ); + case FIELD_TYPES.COMBO_BOX: + return ( + ({ label: v }))} + onCreateOption={onCreateComboOption} + onChange={onComboChange} + onSearchChange={onSearchComboChange} + fullWidth + {...fieldProps} + /> + ); + case FIELD_TYPES.TOGGLE: + return ( + + ); + case FIELD_TYPES.CHECKBOX: + return ( + + ); + case FIELD_TYPES.MULTI_SELECT: + return ( + { + field.setValue(options); + }} + options={field.value as any[]} + {...fieldProps} + > + {(list, search) => ( + + {search} + {list} + + )} + + ); + default: + return ( + + ); + } + }; + + return ( + + {renderField()} + + ); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx b/src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx new file mode 100644 index 0000000000000..7eb3e465e315a --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { EuiDescribedFormGroup, EuiTitle } from '@elastic/eui'; +import { Field as FieldType } from '../hook_form_lib'; +import { Field } from './field'; + +interface Props { + title: string | JSX.Element; + description?: string | JSX.Element; + field?: FieldType; + fieldProps?: Record; + children?: React.ReactNode; +} + +export const FormRow = ({ title, description, field, fieldProps = {}, children }: Props) => { + // If a string is provided, create a default Euititle of size "m" + const _title = + typeof title === 'string' ? ( + +

{title}

+
+ ) : ( + title + ); + + if (!children && !field) { + throw new Error('You need to provide either children or a field to the FormRow'); + } + + return ( + + {children ? children : } + + ); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts new file mode 100644 index 0000000000000..de9e85ebf2e24 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './field'; +export * from './form_row'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts b/src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts new file mode 100644 index 0000000000000..87dd6c95c8840 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Error codes +export const ERR_FIELD_MISSING = 'ERR_FIELD_MISSING'; +export const ERR_MIN_LENGTH = 'ERR_MIN_LENGTH'; +export const ERR_MIN_SELECTION = 'ERR_MIN_SELECTION'; +export const ERR_FIELD_FORMAT = 'ERR_FIELD_FORMAT'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts b/src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts new file mode 100644 index 0000000000000..16f56fbfd2d75 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ERR_FIELD_FORMAT, + ERR_FIELD_MISSING, + ERR_MIN_LENGTH, + ERR_MIN_SELECTION, +} from './constants'; + +export const fieldMissingError = (fieldName: string, message = 'Field missing') => ({ + code: ERR_FIELD_MISSING, + fieldName, + message, +}); + +export const minLengthError = ( + length: number, + message = (error: any) => `Must have a minimun length of ${error.length}.` +) => ({ + code: ERR_MIN_LENGTH, + length, + message, +}); + +export const minSelectionError = ( + length: number, + message = (error: any) => `Must select at least ${error.length} items.` +) => ({ + code: ERR_MIN_SELECTION, + length, + message, +}); + +export const formatError = (format: string, message = 'Format error') => ({ + code: ERR_FIELD_FORMAT, + format, + message, +}); diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts new file mode 100644 index 0000000000000..1fda395a4d7e5 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './errors'; +export * from './constants'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts new file mode 100644 index 0000000000000..861ae32e01875 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState, useEffect, useRef } from 'react'; + +import { Form, FormData } from '../types'; +import { Subscription } from '../lib'; + +interface Props { + children: (formData: FormData) => JSX.Element | null; + form: Form; + pathsToWatch?: string | string[]; +} + +export const FormDataProvider = ({ children, form, pathsToWatch }: Props) => { + const [formData, setFormData] = useState({}); + const previousState = useRef({}); + const subscription = useRef(null); + + useEffect(() => { + let didUnsubscribe = false; + subscription.current = form.__formData$.current.subscribe(data => { + if (didUnsubscribe) { + return; + } + // To avoid re-rendering the children for updates on the form data + // that we are **not** interested in, we can specify one or multiple path(s) + // to watch. + if (pathsToWatch) { + const valuesToWatchToArray = Array.isArray(pathsToWatch) + ? (pathsToWatch as string[]) + : ([pathsToWatch] as string[]); + if (valuesToWatchToArray.some(value => previousState.current[value] !== data[value])) { + previousState.current = data; + setFormData(data); + } + } else { + setFormData(data); + } + }); + + return () => { + didUnsubscribe = true; + subscription.current!.unsubscribe(); + }; + }, [pathsToWatch]); + + return children(formData); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts new file mode 100644 index 0000000000000..307b71f8d86b4 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './use_field'; +export * from './use_array'; +export * from './form_data_provider'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts new file mode 100644 index 0000000000000..0bf7d1df09ed9 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState, useRef } from 'react'; +import { Form } from '../types'; + +interface Props { + path: string; + form: Form; + initialNumberOfItems?: number; + children: (args: { + items: ArrayItem[]; + addItem: () => void; + removeItem: (id: number) => void; + }) => JSX.Element; +} + +export interface ArrayItem { + id: number; + path: string; + isNew: boolean; +} + +export const UseArray = ({ path, form, initialNumberOfItems, children }: Props) => { + const defaultValues = form.__getFieldDefaultValue(path) as any[]; + const uniqueId = useRef(0); + + const getInitialRowsFromValues = (values: any[]): ArrayItem[] => + values.map((_, index) => ({ + id: uniqueId.current++, + path: `${path}.${index}`, + isNew: false, + })); + + const getNewItemAtIndex = (index: number): ArrayItem => ({ + id: uniqueId.current++, + path: `${path}.${index}`, + isNew: true, + }); + + const initialState = defaultValues + ? getInitialRowsFromValues(defaultValues) + : new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i)); + + const [items, setItems] = useState(initialState); + + const updatePaths = (_rows: ArrayItem[]) => + _rows.map( + (row, index) => + ({ + ...row, + path: `${path}.${index}`, + } as ArrayItem) + ); + + const addItem = () => { + setItems(previousItems => { + const itemIndex = previousItems.length; + return [...previousItems, getNewItemAtIndex(itemIndex)]; + }); + }; + + const removeItem = (id: number) => { + setItems(previousItems => { + const updatedItems = previousItems.filter(item => item.id !== id); + return updatePaths(updatedItems); + }); + }; + + return children({ items, addItem, removeItem }); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx new file mode 100644 index 0000000000000..12aee87343c24 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; + +import { Form, Field as FieldType, FieldConfig } from '../types'; +import { useField } from '../hooks'; + +interface Props { + path: string; + config?: FieldConfig; + defaultValue?: unknown; + form: Form; + component?: (({ field }: { field: FieldType } & any) => JSX.Element) | 'input'; + componentProps?: any; + children?: (field: FieldType) => JSX.Element; +} + +export const UseField = ({ + path, + config, + form, + defaultValue = form.__getFieldDefaultValue(path), + component = 'input', + componentProps = {}, + children, +}: Props) => { + if (!config) { + config = form.__readFieldConfigFromSchema(path); + } + + // Don't modify the config object + const configCopy = + typeof defaultValue !== 'undefined' ? { ...config, defaultValue } : { ...config }; + + if (!configCopy.path) { + configCopy.path = path; + } else { + if (configCopy.path !== path) { + throw new Error( + `Field path mismatch. Got "${path}" but field config has "${configCopy.path}".` + ); + } + } + + const field = useField(form, path, configCopy); + + // Remove field from form when it is unmounted or if its path changes + useEffect(() => { + return () => { + form.__removeField(path); + }; + }, [path]); + + // Children prevails over anything else provided. + if (children) { + return children!(field); + } + + if (component === 'input') { + return ( + + ); + } + + return component({ field, ...componentProps }); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts new file mode 100644 index 0000000000000..89ea91485a2d0 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Field types +export const FIELD_TYPES = { + TEXT: 'text', + NUMBER: 'number', + TOGGLE: 'toggle', + CHECKBOX: 'checkbox', + COMBO_BOX: 'comboBox', + SELECT: 'select', + MULTI_SELECT: 'multiSelect', +}; + +// Validation types +export const VALIDATION_TYPES = { + FIELD: 'field', // Default validation error (on the field value) + ASYNC: 'async', // Throw from asynchronous validations + ARRAY_ITEM: 'arrayItem', // If the field value is an Array, this error would be thrown if an _item_ of the array is invalid +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts new file mode 100644 index 0000000000000..6a04a592227f9 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useField } from './use_field'; +export { useForm } from './use_form'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts new file mode 100644 index 0000000000000..b2bac5da64333 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -0,0 +1,385 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState, useEffect, useRef } from 'react'; + +import { + Form, + Field, + FieldConfig, + FieldValidateResponse, + ValidationConfig, + ValidationError, +} from '../types'; +import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; + +export const useField = (form: Form, path: string, config: FieldConfig = {}) => { + const { + type = FIELD_TYPES.TEXT, + defaultValue = '', + label = '', + helpText = '', + validations = [], + formatters = [], + fieldsToValidateOnChange = [path], + isValidationAsync = false, + errorDisplayDelay = form.options.errorDisplayDelay, + serializer = (value: unknown) => value, + deSerializer = (value: unknown) => value, + } = config; + + const [value, setStateValue] = useState( + typeof defaultValue === 'function' ? deSerializer(defaultValue()) : deSerializer(defaultValue) + ); + const [errors, setErrors] = useState([]); + const [isPristine, setPristine] = useState(true); + const [isValidating, setValidating] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + const validateCounter = useRef(0); + const debounceTimeout = useRef(null); + + // -- HELPERS + // ---------------------------------- + + /** + * Filter an array of errors with specific validation type on them + * + * @param _errors The array of errors to filter + * @param validationType The validation type to filter out + */ + const filterErrors = ( + _errors: ValidationError[], + validationTypeToFilterOut: string | string[] = VALIDATION_TYPES.FIELD + ): ValidationError[] => { + const validationTypeToArray = Array.isArray(validationTypeToFilterOut) + ? (validationTypeToFilterOut as string[]) + : ([validationTypeToFilterOut] as string[]); + + return _errors.filter(error => + validationTypeToArray.every(_type => error.validationType !== _type) + ); + }; + + const runFormatters = (input: unknown): unknown => { + const isEmptyString = typeof input === 'string' && input.trim() === ''; + + if (isEmptyString) { + return input; + } + return formatters.reduce((output, formatter) => formatter(output), input); + }; + + const onValueChange = () => { + if (isPristine) { + setPristine(false); + } + setIsUpdating(true); + + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + debounceTimeout.current = setTimeout(() => { + setIsUpdating(false); + }, errorDisplayDelay); + }; + + const validateSync = ({ + formData, + value: valueToValidate, + validationTypeToValidate, + }: { + formData: any; + value: unknown; + validationTypeToValidate?: string; + }): ValidationError[] => { + const validationErrors: ValidationError[] = []; + let skip = false; + + const runValidation = ({ + validator, + exitOnFail, + type: validationType = VALIDATION_TYPES.FIELD, + }: ValidationConfig) => { + if ( + skip || + (typeof validationTypeToValidate !== 'undefined' && + validationType !== validationTypeToValidate) + ) { + return; + } + let validationResult; + + try { + validationResult = validator({ + value: (valueToValidate as unknown) as string, + errors: validationErrors, + form, + formData, + path, + }); + + if (validationResult && exitOnFail !== false) { + throw validationResult; + } + } catch (error) { + // If an error is thrown, skip the rest of the validations + skip = true; + validationResult = error; + } + + return validationResult; + }; + + // Execute each validations for the field sequentially + validations.forEach(validation => { + const validationResult = runValidation(validation); + + if (validationResult) { + validationErrors.push({ + ...validationResult, + validationType: validation.type || VALIDATION_TYPES.FIELD, + }); + } + }); + + return validationErrors; + }; + + const validateAsync = async ({ + formData, + value: valueToValidate, + validationTypeToValidate, + }: { + formData: any; + value: unknown; + validationTypeToValidate?: string; + }): Promise => { + const validationErrors: ValidationError[] = []; + let skip = false; + + // By default, for fields that have an asynchronous validation + // we will clear the errors as soon as the field value changes. + clearErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); + + const runValidation = async ({ + validator, + exitOnFail, + type: validationType = VALIDATION_TYPES.FIELD, + }: ValidationConfig) => { + if ( + skip || + (typeof validationTypeToValidate !== 'undefined' && + validationType !== validationTypeToValidate) + ) { + return; + } + let validationResult; + + try { + validationResult = await validator({ + value: (valueToValidate as unknown) as string, + errors: validationErrors, + form, + formData, + path, + }); + + if (validationResult && exitOnFail !== false) { + throw validationResult; + } + } catch (error) { + // If an error is thrown, skip the rest of the validations + skip = true; + validationResult = error; + } + + return validationResult; + }; + + // Sequencially execute all the validations for the field + for (const validation of validations) { + const validationResult = await runValidation(validation); + + if (validationResult) { + validationErrors.push({ + ...validationResult, + validationType: validation.type || VALIDATION_TYPES.FIELD, + }); + } + } + + return validationErrors; + }; + + // -- API + // ---------------------------------- + const clearErrors: Field['clearErrors'] = (validationType = VALIDATION_TYPES.FIELD) => { + setErrors(previousErrors => filterErrors(previousErrors, validationType)); + }; + + /** + * Validate a form field, running all its validations. + * If a validationType is provided then only that validation will be executed, + * skipping the other type of validation that might exist. + */ + const validate: Field['validate'] = (validationData = {}) => { + const { + formData = form.getFormData({ unflatten: false }), + value: valueToValidate = value, + validationType, + } = validationData; + + setValidating(true); + + // By the time our validate function has reached completion, it’s possible + // that validate() will have been called again. If this is the case, we need + // to ignore the results of this invocation and only use the results of + // the most recent invocation to update the error state for a field + const validateIteration = ++validateCounter.current; + + const onValidationResult = (validationErrors: ValidationError[]): FieldValidateResponse => { + if (validateIteration === validateCounter.current) { + // This is the most recent invocation + setValidating(false); + // Update the errors array + setErrors(previousErrors => { + // First filter out the validation type we are currently validating + const filteredErrors = filterErrors(previousErrors, validationType); + return [...filteredErrors, ...validationErrors]; + }); + } + return { + isValid: validationErrors.length === 0, + errors: validationErrors, + }; + }; + + if (isValidationAsync) { + return validateAsync({ + formData, + value: valueToValidate, + validationTypeToValidate: validationType, + }).then(onValidationResult); + } else { + const validationErrors = validateSync({ + formData, + value: valueToValidate, + validationTypeToValidate: validationType, + }); + return onValidationResult(validationErrors); + } + }; + + /** + * Handler to change the field value + * + * @param newValue The new value to assign to the field + */ + const setValue: Field['setValue'] = newValue => { + onValueChange(); + + const formattedValue = runFormatters(newValue); + setStateValue(formattedValue); + + // Update the form data observable + form.__updateFormDataAt(path, getOutputValue(formattedValue)); + }; + + const _setErrors: Field['setErrors'] = _errors => { + setErrors(_errors.map(error => ({ validationType: VALIDATION_TYPES.FIELD, ...error }))); + }; + + /** + * Form "onChange" event handler + * + * @param event Form input change event + */ + const onChange: Field['onChange'] = event => { + const newValue = {}.hasOwnProperty.call(event!.target, 'checked') + ? event.target.checked + : event.target.value; + + setValue(newValue); + }; + + /** + * As we can have multiple validation types (FIELD, ASYNC, ARRAY_ITEM), this + * method allows us to retrieve error messages for certain types of validation. + * + * For example, if we want to validation error messages to be displayed when the user clicks the "save" button + * _but_ in case of an asynchronous validation (for ex. an HTTP request that would validate an index name) we + * want to immediately display the error message, we would have 2 types of validation: FIELD & ASYNC + * + * @param validationType The validation type to return error messages from + */ + const getErrorsMessages: Field['getErrorsMessages'] = (args = {}) => { + const { errorCode, validationType = VALIDATION_TYPES.FIELD } = args; + const errorMessages = errors.reduce((messages, error) => { + const isSameErrorCode = errorCode && error.code === errorCode; + const isSamevalidationType = + error.validationType === validationType || + (validationType === VALIDATION_TYPES.FIELD && + !{}.hasOwnProperty.call(error, 'validationType')); + + if (isSameErrorCode || (typeof errorCode === 'undefined' && isSamevalidationType)) { + return messages ? `${messages}, ${error.message}` : (error.message as string); + } + return messages; + }, ''); + + return errorMessages ? errorMessages : null; + }; + + const getOutputValue: Field['__getOutputValue'] = (rawValue = value) => serializer(rawValue); + + // -- EFFECTS + // ---------------------------------- + useEffect(() => { + if (isPristine) { + // Avoid validate on mount + return; + } + form.__validateFields(fieldsToValidateOnChange); + }, [value]); + + const field: Field = { + path, + type, + label, + helpText, + value, + errors, + form, + isPristine, + isValidating, + isUpdating, + onChange, + getErrorsMessages, + setValue, + setErrors: _setErrors, + clearErrors, + validate, + __getOutputValue: getOutputValue, + }; + + form.__addField(field); + + return field; +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts new file mode 100644 index 0000000000000..413052c0f504b --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -0,0 +1,192 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState, useRef } from 'react'; + +import { Form, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; +import { getAt, mapFormFields, unflattenObject, Subject } from '../lib'; + +const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500; + +export const useForm = ( + formConfig: FormConfig | undefined = {} +): { form: Form } => { + const { + onSubmit, + schema, + defaultValue = {}, + serializer = (data: any) => data, + deSerializer = (data: any) => data, + options = { errorDisplayDelay: DEFAULT_ERROR_DISPLAY_TIMEOUT, stripEmptyFields: true }, + } = formConfig; + const defaultValueDeSerialized = + Object.keys(defaultValue).length === 0 ? defaultValue : deSerializer(defaultValue); + const [isSubmitted, setSubmitted] = useState(false); + const [isSubmitting, setSubmitting] = useState(false); + const [isValid, setIsValid] = useState(true); + const fieldsRefs = useRef({}); + + // formData$ is an observable we can subscribe to in order to receive live + // update of the raw form data. As an observable it does not trigger any React + // render(). + // The component is the one in charge of reading this observable + // and updating its state to trigger the necessary view render. + const formData$ = useRef>(new Subject({} as T)); + + // -- HELPERS + // ---------------------------------- + const fieldsToArray = () => Object.values(fieldsRefs.current); + + const stripEmptyFields = (fields: FieldsMap): FieldsMap => { + if (options.stripEmptyFields) { + return Object.entries(fields).reduce( + (acc, [key, field]) => { + if (field.value !== '') { + acc[key] = field; + } + return acc; + }, + {} as FieldsMap + ); + } + return fields; + }; + + // -- API + // ---------------------------------- + const getFormData: Form['getFormData'] = (getDataOptions = { unflatten: true }) => + getDataOptions.unflatten + ? (unflattenObject( + mapFormFields(stripEmptyFields(fieldsRefs.current), field => field.__getOutputValue()) + ) as T) + : Object.entries(fieldsRefs.current).reduce( + (acc, [key, field]) => ({ + ...acc, + [key]: field.__getOutputValue(), + }), + {} as T + ); + + const updateFormDataAt: Form['__updateFormDataAt'] = (path, value) => { + const currentFormData = formData$.current.value; + formData$.current.next({ ...currentFormData, [path]: value }); + return formData$.current.value; + }; + + const validateFields: Form['__validateFields'] = async fieldNames => { + const fieldsToValidate = fieldNames + ? fieldNames.map(name => fieldsRefs.current[name]).filter(field => field !== undefined) + : fieldsToArray().filter(field => field.isPristine); + + const formData = getFormData({ unflatten: false }); + + await Promise.all(fieldsToValidate.map(field => field.validate({ formData }))); + + const isFormValid = fieldsToArray().every( + field => field.getErrorsMessages() === null && !field.isValidating + ); + setIsValid(isFormValid); + + return isFormValid; + }; + + const addField: Form['__addField'] = field => { + fieldsRefs.current[field.path] = field; + + // Only update the formData if the path does not exist (it is the _first_ time + // the field is added), to avoid entering an infinite loop when the form is re-rendered. + if (!{}.hasOwnProperty.call(formData$.current.value, field.path)) { + updateFormDataAt(field.path, field.__getOutputValue()); + } + }; + + const removeField: Form['__removeField'] = _fieldNames => { + const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames]; + const currentFormData = { ...formData$.current.value } as FormData; + + fieldNames.forEach(name => { + delete fieldsRefs.current[name]; + delete currentFormData[name]; + }); + + formData$.current.next(currentFormData as T); + }; + + const setFieldValue: Form['setFieldValue'] = (fieldName, value) => { + fieldsRefs.current[fieldName].setValue(value); + }; + + const setFieldErrors: Form['setFieldErrors'] = (fieldName, errors) => { + fieldsRefs.current[fieldName].setErrors(errors); + }; + + const getFields: Form['getFields'] = () => fieldsRefs.current; + + const getFieldDefaultValue: Form['__getFieldDefaultValue'] = fieldName => + getAt(fieldName, defaultValueDeSerialized, false); + + const readFieldConfigFromSchema: Form['__readFieldConfigFromSchema'] = fieldName => { + const config = (getAt(fieldName, schema ? schema : {}, false) as FieldConfig) || {}; + + return config; + }; + + const onSubmitForm: Form['onSubmit'] = async e => { + if (e) { + e.preventDefault(); + } + + setSubmitting(true); + setSubmitted(true); // User has attempted to submit the form at least once + + const isFormValid = await validateFields(); + const formData = serializer(getFormData() as T); + + if (onSubmit) { + await onSubmit(formData, isFormValid); + } + + setSubmitting(false); + + return { data: formData, isValid: isFormValid }; + }; + + const form: Form = { + isSubmitted, + isSubmitting, + isValid, + options, + onSubmit: onSubmitForm, + setFieldValue, + setFieldErrors, + getFields, + getFormData, + __formData$: formData$, + __updateFormDataAt: updateFormDataAt, + __readFieldConfigFromSchema: readFieldConfigFromSchema, + __addField: addField, + __removeField: removeField, + __validateFields: validateFields, + __getFieldDefaultValue: getFieldDefaultValue, + }; + + return { + form, + }; +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts new file mode 100644 index 0000000000000..68c7cba15931a --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Only export the useForm hook. The "useField" hook is for internal use +// as the consumer of the library must use the component +export { useForm } from './hooks'; +export { fieldFormatters } from './lib'; + +export * from './components'; +export * from './constants'; +export * from './types'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts new file mode 100644 index 0000000000000..830c7725c95f4 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { Subject, Subscription } from './subject'; +export * from './utils'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts new file mode 100644 index 0000000000000..8ccc67a133355 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +type Listener = (value: T) => void; + +export interface Subscription { + unsubscribe: () => void; +} + +export class Subject { + private callbacks: Set> = new Set(); + value: T; + + constructor(value: T) { + this.value = value; + } + + subscribe(fn: Listener): Subscription { + this.callbacks.add(fn); + + setTimeout(() => { + fn(this.value); + }); + + const unsubscribe = () => this.callbacks.delete(fn); + return { + unsubscribe, + }; + } + + next(value: T) { + this.value = value; + this.callbacks.forEach(fn => fn(value)); + } +} diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts new file mode 100644 index 0000000000000..1bd4252b5dfed --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Field } from '../types'; + +const numRegEx = /^\d+$/; + +const isNumber = (val: string) => numRegEx.test(val); + +export const getAt = (path: string, object: any, throwIfNotFound = true): unknown => { + const pathToArray = path.split('.'); + const value = object[pathToArray[0]]; + + if (pathToArray.length === 1) { + return value; + } + + if (value !== null && typeof value === 'object') { + return getAt(pathToArray.slice(1).join('.'), value, throwIfNotFound); + } + + if (throwIfNotFound) { + throw new Error(`Can't access path "${path}" on ${JSON.stringify(object)}`); + } + + return undefined; +}; + +const setAt = (path: string, object: any, value: unknown, createUnknownPath = true): any => { + const pathToArray = path.split('.'); + + if (pathToArray.length === 1) { + object[pathToArray[0]] = value; + return object; + } + + let target = object; + + pathToArray.slice(0, -1).forEach((key, i) => { + if (!{}.hasOwnProperty.call(target, key)) { + if (createUnknownPath) { + // If the path segment is a number, we create an Array + // otherwise we create an object. + target[key] = isNumber(pathToArray[i + 1]) ? [] : {}; + } else { + throw new Error(`Can't set value "${value}" at "${path}" on ${JSON.stringify(object)}`); + } + } + + target = target[key]; + + if (target === null || (typeof target !== 'object' && !Array.isArray(target))) { + throw new Error( + `Can't set value "${value}" on a primitive. Path provided: "${path}", target: ${JSON.stringify( + object + )}` + ); + } + }); + + const keyToSet = pathToArray[pathToArray.length - 1]; + target[keyToSet] = value; + + return object; +}; + +export const unflattenObject = (object: any) => + Object.entries(object).reduce((acc, [key, field]) => { + setAt(key, acc, field); + return acc; + }, {}); + +/** + * Helper to map the object of fields to any of its value + * + * @param formFields key value pair of path and form Fields + * @param fn Iterator function to execute on the field + */ +export const mapFormFields = (formFields: Record, fn: (field: Field) => any) => + Object.entries(formFields).reduce( + (acc, [key, field]) => { + acc[key] = fn(field); + return acc; + }, + {} as Record + ); diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts new file mode 100644 index 0000000000000..32b6e06fd5e4c --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts @@ -0,0 +1,149 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChangeEvent, FormEvent, MouseEvent, MutableRefObject } from 'react'; +import { Subject } from './lib'; + +export interface Form { + readonly isSubmitted: boolean; + readonly isSubmitting: boolean; + readonly isValid: boolean; + readonly options: FormOptions; + onSubmit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; + setFieldValue: (fieldName: string, value: FieldValue) => void; + setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; + getFields: () => FieldsMap; + readonly __formData$: MutableRefObject>; + __addField: (field: Field) => void; + __removeField: (fieldNames: string | string[]) => void; + __validateFields: (fieldNames?: string[]) => Promise; + getFormData: (options?: { unflatten?: boolean }) => T; + __updateFormDataAt: (field: string, value: unknown) => T; + __getFieldDefaultValue: (fieldName: string) => unknown; + __readFieldConfigFromSchema: (fieldName: string) => FieldConfig; +} + +export interface FormSchema { + [key: string]: FormSchemaEntry; +} +type FormSchemaEntry = + | FieldConfig + | Array> + | { [key: string]: FieldConfig | Array> | FormSchemaEntry }; + +export interface FormConfig { + onSubmit?: (data: T, isFormValid: boolean) => void; + schema?: FormSchema; + defaultValue?: Partial; + serializer?: SerializerFunc; + deSerializer?: SerializerFunc; + options?: FormOptions; +} + +export interface FormOptions { + errorDisplayDelay: number; + /** + * Remove empty string field ("") from form data + */ + stripEmptyFields: boolean; +} + +export interface Field { + readonly path: string; + readonly label?: string; + readonly helpText?: string; + readonly type: string; + readonly value: unknown; + readonly errors: ValidationError[]; + readonly isPristine: boolean; + readonly isValidating: boolean; + readonly isUpdating: boolean; + readonly form: Form; + getErrorsMessages: (args?: { + validationType?: 'field' | string; + errorCode?: string; + }) => string | null; + onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void; + setValue: (value: FieldValue) => void; + setErrors: (errors: ValidationError[]) => void; + clearErrors: (type?: string | string[]) => void; + validate: (validateData?: { + formData?: any; + value?: unknown; + validationType?: string; + }) => FieldValidateResponse | Promise; + __getOutputValue: (rawValue?: unknown) => unknown; +} + +export interface FieldConfig { + readonly path?: string; + readonly label?: string; + readonly helpText?: string; + readonly type?: HTMLInputElement['type']; + readonly defaultValue?: unknown; + readonly validations?: Array>; + readonly formatters?: FormatterFunc[]; + readonly deSerializer?: SerializerFunc; + readonly serializer?: SerializerFunc; + readonly fieldsToValidateOnChange?: string[]; + readonly isValidationAsync?: boolean; + readonly errorDisplayDelay?: number; +} + +export interface FieldsMap { + [key: string]: Field; +} + +export type FormSubmitHandler = (formData: T, isValid: boolean) => Promise; + +export interface ValidationError { + message: string | ((error: ValidationError) => string); + code?: string; + validationType?: string; + [key: string]: any; +} + +export type ValidationFunc = (data: { + path: string; + value: unknown; + form: Form; + formData: T; + errors: readonly ValidationError[]; +}) => ValidationError | void | undefined | Promise; + +export interface FieldValidateResponse { + isValid: boolean; + errors: ValidationError[]; +} + +export type SerializerFunc = (value: any) => T; + +export type FormData = Record; + +type FormatterFunc = (value: any) => unknown; + +// We set it as unknown as a form field can be any of any type +// string | number | boolean | string[] ... +type FieldValue = unknown; + +export interface ValidationConfig { + validator: ValidationFunc; + type?: string; + exitOnFail?: boolean; +} diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts new file mode 100644 index 0000000000000..a33b03853aeab --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Option } from '@elastic/eui/src/components/selectable/types'; +import { SerializerFunc } from '../hook_form_lib'; + +type FuncType = (selectOptions: Option[]) => SerializerFunc; + +// This deSerializer takes the previously selected options and map them +// against the default select options values. +export const multiSelectSelectedValueToOptions: FuncType = selectOptions => defaultFormValue => { + // If there are no default form value, it means that no previous value has been selected. + if (!defaultFormValue) { + return selectOptions; + } + + return (selectOptions as Option[]).map(option => ({ + ...option, + checked: (defaultFormValue as string[]).includes(option.label) ? 'on' : undefined, + })); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts new file mode 100644 index 0000000000000..c82e46fd074ad --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// ------------------------- NOTE -------------------------------------- +// Add here any custom field formatters that +// are not common enough to go in the "hook_form_lib" field_formatters.ts +// ----------------------------------------------------------------------- diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts new file mode 100644 index 0000000000000..e6beb012d1e14 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ValidationFunc } from '../../hook_form_lib'; +import { containsChars } from '../../../validators/string'; +import { formatError } from '../../errors'; + +export const containsCharsField = ({ + message, + chars, +}: { + message: string; + chars: string | string[]; +}) => (...args: Parameters): ReturnType => { + const [{ value }] = args; + + const { doesContain, charsFound } = containsChars(chars)(value as string); + + if (doesContain) { + return { + ...formatError('CONTAINS_INVALID_CHARS', message), + charsFound, + }; + } +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts new file mode 100644 index 0000000000000..fccf28c8e0b4d --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ValidationFunc } from '../../hook_form_lib'; +import { isEmptyString } from '../../../validators/string'; +import { isEmptyArray } from '../../../validators/array'; +import { fieldMissingError } from '../../errors'; + +export const emptyField = (message: string) => ( + ...args: Parameters +): ReturnType => { + const [{ value, path }] = args; + + if (typeof value === 'string') { + return isEmptyString(value) ? { ...fieldMissingError(path), message } : undefined; + } + + if (Array.isArray(value)) { + return isEmptyArray(value) ? { ...fieldMissingError(path), message } : undefined; + } +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts new file mode 100644 index 0000000000000..ee47ae7b6cc07 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './empty_field'; +export * from './min_length'; +export * from './min_selectable_selection'; +export * from './url'; +export * from './index_name'; +export * from './contains_char'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts new file mode 100644 index 0000000000000..a70ec0d3036f0 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Note: we can't import from "ui/indices" as the TS Type definition don't exist +// import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; +import { ValidationFunc } from '../../hook_form_lib'; +import { startsWith, containsChars } from '../../../validators/string'; +import { formatError } from '../../errors'; + +const INDEX_ILLEGAL_CHARACTERS = ['\\', '/', '?', '"', '<', '>', '|', '*']; + +export const indexNameField = (...args: Parameters): ReturnType => { + const [{ value }] = args; + + if (startsWith('.')(value as string)) { + return formatError('INDEX_NAME', 'Cannot start with a dot (".").'); + } + + const { doesContain: doesContainSpaces } = containsChars(' ')(value as string); + if (doesContainSpaces) { + return formatError('INDEX_NAME', 'Cannot contain spaces.'); + } + + const { charsFound, doesContain } = containsChars(INDEX_ILLEGAL_CHARACTERS)(value as string); + if (doesContain) { + return formatError( + 'INDEX_NAME', + `Cannot contain the following characters: "${charsFound.join(',')}."` + ); + } +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts new file mode 100644 index 0000000000000..3f6aced2a81a7 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ValidationFunc } from '../../hook_form_lib'; +import { hasMinLengthString } from '../../../validators/string'; +import { hasMinLengthArray } from '../../../validators/array'; +import { minLengthError } from '../../errors'; + +export const minLengthField = (length = 0) => ( + ...args: Parameters +): ReturnType => { + const [{ value }] = args; + + if (Array.isArray(value)) { + return hasMinLengthArray(length)(value) ? undefined : minLengthError(length); + } + return hasMinLengthString(length)((value as string).trim()) ? undefined : minLengthError(length); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts new file mode 100644 index 0000000000000..23267f49cc22b --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Option } from '@elastic/eui/src/components/selectable/types'; + +import { ValidationFunc } from '../../hook_form_lib'; +import { hasMinLengthArray } from '../../../validators/array'; +import { minSelectionError } from '../../errors'; +import { multiSelectOptionsToSelectedValue } from '../../lib'; + +/** + * Validator to validate that a EuiSelectable has a minimum number + * of items selected. + * @param total Minimum number of items + */ +export const minSelectableSelectionField = (total = 0) => ( + ...args: Parameters +): ReturnType => { + const [{ value }] = args; + + return hasMinLengthArray(total)(multiSelectOptionsToSelectedValue(value as Option[])) + ? undefined + : minSelectionError(total); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts new file mode 100644 index 0000000000000..2fdb173a41e67 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ValidationFunc } from '../../hook_form_lib'; +import { isEmptyString, isUrl } from '../../../validators/string'; +import { formatError } from '../../errors'; + +export const urlField = (allowEmpty = false) => ( + ...args: Parameters +): ReturnType => { + const [{ value }] = args; + + if (typeof value !== 'string') { + return formatError('URL'); + } + + if (allowEmpty && isEmptyString(value)) { + return; + } + + return isUrl(value) ? undefined : formatError('URL'); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts new file mode 100644 index 0000000000000..4d91dc873f094 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './de_serializers'; +export * from './serializers'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts new file mode 100644 index 0000000000000..3d4517d65923c --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Output transforms are functions that will be called + * with the form field value whenever we access the form data object. (with `form.getFormData()`) + * + * This allows us to have a different object/array as field `value` + * from the desired outputed form data. + * + * Example: + * ```ts + * myField.value = [{ label: 'index_1', isSelected: true }, { label: 'index_2', isSelected: false }] + * const serializer = (value) => ( + * value.filter(v => v.selected).map(v => v.label) + * ); + * + * // When serializing the form data, the following array will be returned + * form.getFormData() -> { myField: ['index_1'] } + * ```` + */ + +import { Option } from '@elastic/eui/src/components/selectable/types'; +import { SerializerFunc } from '../hook_form_lib'; + +/** + * Return an array of labels of all the options that are selected + * + * @param value The Eui Selectable options array + */ +export const multiSelectOptionsToSelectedValue: SerializerFunc = ( + options: Option[] +): string[] => options.filter(option => option.checked === 'on').map(option => option.label); diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts new file mode 100644 index 0000000000000..a03b307427b4d --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const hasMaxLengthArray = (length = 5) => (value: any[]): boolean => value.length <= length; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts new file mode 100644 index 0000000000000..aaa8b810bed1a --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const hasMinLengthArray = (length = 1) => (value: any[]): boolean => value.length >= length; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts new file mode 100644 index 0000000000000..cb724e048c9e0 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './has_max_length'; +export * from './has_min_length'; +export * from './is_empty'; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts new file mode 100644 index 0000000000000..f97caeb9d4e4c --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const isEmptyArray = (value: any[]): boolean => value.length === 0; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts new file mode 100644 index 0000000000000..869a2477cfd4b --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const containsChars = (chars: string | string[]) => (value: string) => { + const charToArray = Array.isArray(chars) ? (chars as string[]) : ([chars] as string[]); + + const charsFound = charToArray.reduce( + (acc, char) => (value.includes(char) ? [...acc, char] : acc), + [] as string[] + ); + + return { + charsFound, + doesContain: charsFound.length > 0, + }; +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts new file mode 100644 index 0000000000000..58ba1ccfdc388 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const endsWith = (char: string) => (value: string) => value.endsWith(char); diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts new file mode 100644 index 0000000000000..371ccf36e8151 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const hasMaxLengthString = (length: number) => (str: string) => str.length <= length; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts new file mode 100644 index 0000000000000..bc12277c68284 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const hasMinLengthString = (length: number) => (str: string) => str.length >= length; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts new file mode 100644 index 0000000000000..4c34ebbb4886d --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './contains_chars'; +export * from './ends_with'; +export * from './has_max_length'; +export * from './has_min_length'; +export * from './is_empty'; +export * from './is_url'; +export * from './starts_with'; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts new file mode 100644 index 0000000000000..b82eda817b451 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const isEmptyString = (value: string) => value.trim() === ''; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts new file mode 100644 index 0000000000000..5954760bf106a --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/; +const localhostDomainRE = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/; +const nonLocalhostDomainRE = /^[^\s\.]+\.\S{2,}$/; + +export const isUrl = (string: string) => { + if (typeof string !== 'string') { + return false; + } + + const match = string.match(protocolAndDomainRE); + if (!match) { + return false; + } + + const everythingAfterProtocol = match[1]; + if (!everythingAfterProtocol) { + return false; + } + + if ( + localhostDomainRE.test(everythingAfterProtocol) || + nonLocalhostDomainRE.test(everythingAfterProtocol) + ) { + return true; + } + + return false; +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts new file mode 100644 index 0000000000000..ffd840a07eebe --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const startsWith = (char: string) => (value: string) => value.startsWith(char); diff --git a/x-pack/legacy/plugins/index_management/public/app.js b/x-pack/legacy/plugins/index_management/public/app.js index 8f25183a21bc1..0936056d2718d 100644 --- a/x-pack/legacy/plugins/index_management/public/app.js +++ b/x-pack/legacy/plugins/index_management/public/app.js @@ -9,6 +9,7 @@ import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; import { BASE_PATH, UIM_APP_LOAD } from '../common/constants'; import { IndexManagementHome } from './sections/home'; import { trackUiMetric } from './services'; +import { CreateIndex } from './sections/create_index'; export const App = () => { useEffect(() => trackUiMetric('loaded', UIM_APP_LOAD), []); @@ -23,6 +24,7 @@ export const App = () => { // Exoprt this so we can test it with a different router. export const AppWithoutRouter = () => ( + diff --git a/x-pack/legacy/plugins/index_management/public/sections/create_index/create_index.tsx b/x-pack/legacy/plugins/index_management/public/sections/create_index/create_index.tsx new file mode 100644 index 0000000000000..ed34acfdbace5 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/create_index/create_index.tsx @@ -0,0 +1,113 @@ +/* + * 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, { useRef, useState } from 'react'; +import { EuiPageContent, EuiButton, EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { MappingsEditor, Mappings } from '../../../static/ui'; + +type GetMappingsEditorDataHandler = () => Promise<{ isValid: boolean; data: Mappings }>; + +const initialData = { + dynamic: 'strict', + date_detection: false, + numeric_detection: true, + dynamic_date_formats: ['MM/dd/yyyy'], + properties: { + title: { + type: 'text', + store: true, + index: false, + fielddata: true, + fields: { + raw: { + type: 'keyword', + store: false, + index: true, + doc_values: true, + }, + }, + }, + myObject: { + type: 'object', + dynamic: true, + enabled: true, + properties: { + prop1: { + type: 'text', + store: false, + index: true, + fielddata: true, + }, + prop2: { + type: 'text', + store: true, + index: true, + fielddata: false, + }, + }, + }, + }, +}; + +// TODO: find solution when going back to a previsouly set type + +export const CreateIndex = () => { + const getMappingsEditorData = useRef(() => + Promise.resolve({ + isValid: true, + data: {}, + }) + ); + const [mappings, setMappings] = useState(initialData); + const [isMappingsValid, setIsMappingsValid] = useState(true); + + const setGetMappingsEditorDataHandler = (handler: GetMappingsEditorDataHandler) => + (getMappingsEditorData.current = handler); + + const onClick = async () => { + const { isValid, data } = await getMappingsEditorData.current(); + // console.log(isValid, data); + setIsMappingsValid(isValid); + setMappings(data); + }; + + return ( + + +

Index Mappings

+
+ + + + + + + Send form + + + +
+ + + +

Mappings editor data:

+
+ + + {isMappingsValid ? ( +
+          {JSON.stringify(mappings, null, 2)}
+        
+ ) : ( +
The mappings JSON data is not valid.
+ )} +
+ ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts b/x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts new file mode 100644 index 0000000000000..2b556147894da --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './create_index'; From bd59da71fb8a3a34458dcdb64284f63b656ac1e2 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 1 Aug 2019 17:22:46 -0700 Subject: [PATCH 2/2] Add a read-only POC. --- .../mappings_editor/mappings_editor.tsx | 3 + .../mappings_editor/read_only_fields.tsx | 157 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/read_only_fields.tsx diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 9eb1a97025f6c..f0ecc307b4f3f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -20,6 +20,7 @@ import { PropertiesManager } from './components'; import { propertiesArrayToObject, propertiesObjectToArray } from './helpers'; import { dataTypesDefinition, getTypeFromSubType } from './config'; import { DYNAMIC_SETTING_OPTIONS } from './constants'; +import { childrenToArray, ReadOnlyFields } from './read_only_fields'; interface Props { setGetDataHandler: (handler: () => Promise<{ isValid: boolean; data: Mappings }>) => void; @@ -82,6 +83,7 @@ export const MappingsEditor = ({ setGetDataHandler(form.onSubmit); }, [form]); + console.log('defaultValue', defaultValue) return ( {/* Global Mappings configuration */} @@ -99,6 +101,7 @@ export const MappingsEditor = ({ + {/* Document fields */}

Document fields

diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/read_only_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/read_only_fields.tsx new file mode 100644 index 0000000000000..8691629a92461 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/read_only_fields.tsx @@ -0,0 +1,157 @@ +/* + * 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, { Fragment, useState } from 'react'; +import { + EuiText, + EuiFlexItem, + EuiFlexGroup, + EuiAccordion, + EuiBadge, +} from '@elastic/eui'; + +export function childrenToArray(children) { + if (!children) { + return []; + } + + return Object.keys(children).map(name => ({ + name, + ...children[name] + })); +} + +function isFieldOpen(field, children) { + // Of course a selected item is open. + if (field.isSelected) { + return true; + } + + // The item has to be open if it has a child that's open. + if (children) { + return children.some(isFieldOpen); + } +}; + +const ReadOnlyField = ({ + children, + onToggle, + isOpen, + isSelected, + isParent, + fields, + depth, + ...rest +}) => { + const { + type, + } = rest; + + console.log('rest', rest) + + const badges = ( + + {['index', 'doc_values', 'store'].reduce((accum, field) => { + if (rest[field]) { + accum.push( + + {field} + + ); + } + return accum; + }, [])} + + ); + + const accordion = isParent ? ( + +
+ {fields} +
+
+ ) : null; + + return ( +
+ + + {children} + + + + + {badges} + + {type} + + + + + + {accordion} +
+ ); +}; + +export const ReadOnlyFields = ({ + fields +}) => { + const [setFieldSelection, selectedFields] = useState({}); + + function toggleFieldSelection(name) { + if (selectedFields[name]) { + delete selectedFields[name]; + } else { + selectedFields[name] = true; + } + setFieldSelection(selectedFields); + } + console.log('fields', fields) + function renderTree(fields, depth = 0) { + return fields.map(field => { + const { + name, + properties, + fields, + ...rest + } = field; + + const children = childrenToArray(properties || fields); + + // Root items are always open. + const isOpen = depth === 0 ? true : isFieldOpen(field, children); + + let renderedFields; + + if (children) { + renderedFields = renderTree(children, depth + 1); + } + + return ( + toggleFieldSelection(name)} + {...rest} + > + {name} + + ); + }); + } + return ( +
+ {renderTree(fields)} +
+ ); +};