/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Any modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * 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, { PureComponent, Fragment } from 'react'; import classNames from 'classnames'; import 'brace/theme/textmate'; import 'brace/mode/markdown'; import 'brace/mode/json'; import { EuiBadge, EuiCode, EuiCodeBlock, EuiScreenReaderOnly, EuiCodeEditor, EuiDescribedFormGroup, EuiFieldNumber, EuiFieldText, EuiFilePicker, EuiFormRow, EuiIconTip, EuiImage, EuiLink, EuiSpacer, EuiText, EuiSelect, EuiSwitch, EuiSwitchEvent, EuiToolTip, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; import { FieldSetting, FieldState } from '../../types'; import { isDefaultValue } from '../../lib'; import { UiSettingsType, ImageValidation, StringValidationRegex, DocLinksStart, ToastsStart, } from '../../../../../../core/public'; interface FieldProps { setting: FieldSetting; handleChange: (name: string, value: FieldState) => void; enableSaving: boolean; dockLinks: DocLinksStart['links']; toasts: ToastsStart; clearChange?: (name: string) => void; unsavedChanges?: FieldState; loading?: boolean; } export const getEditableValue = ( type: UiSettingsType, value: FieldSetting['value'], defVal?: FieldSetting['defVal'] ) => { const val = value === null || value === undefined ? defVal : value; switch (type) { case 'array': return (val as string[]).join(', '); case 'boolean': return !!val; case 'number': return Number(val); case 'image': return val; default: return val || ''; } }; export class Field extends PureComponent { private changeImageForm = React.createRef(); getDisplayedDefaultValue( type: UiSettingsType, defVal: FieldSetting['defVal'], optionLabels: Record = {} ) { if (defVal === undefined || defVal === null || defVal === '') { return 'null'; } switch (type) { case 'array': return (defVal as string[]).join(', '); case 'select': return optionLabels.hasOwnProperty(String(defVal)) ? optionLabels[String(defVal)] : String(defVal); default: return String(defVal); } } handleChange = (unsavedChanges: FieldState) => { this.props.handleChange(this.props.setting.name, unsavedChanges); }; resetField = () => { const { type, defVal } = this.props.setting; if (type === 'image') { this.cancelChangeImage(); return this.handleChange({ value: getEditableValue(type, defVal), changeImage: true, }); } return this.handleChange({ value: getEditableValue(type, defVal) }); }; componentDidUpdate(prevProps: FieldProps) { if ( prevProps.setting.type === 'image' && prevProps.unsavedChanges?.value && !this.props.unsavedChanges?.value ) { this.cancelChangeImage(); } } onCodeEditorChange = (value: string) => { const { defVal, type } = this.props.setting; let newUnsavedValue; let errorParams = {}; switch (type) { case 'json': const isJsonArray = Array.isArray(JSON.parse((defVal as string) || '{}')); newUnsavedValue = value.trim() || (isJsonArray ? '[]' : '{}'); try { JSON.parse(newUnsavedValue); } catch (e) { errorParams = { error: i18n.translate('advancedSettings.field.codeEditorSyntaxErrorMessage', { defaultMessage: 'Invalid JSON syntax', }), isInvalid: true, }; } break; default: newUnsavedValue = value; } this.handleChange({ value: newUnsavedValue, ...errorParams, }); }; onFieldChangeSwitch = (e: EuiSwitchEvent) => { return this.onFieldChange(e.target.checked); }; onFieldChangeEvent = (e: React.ChangeEvent) => this.onFieldChange(e.target.value); onFieldChange = (targetValue: any) => { const { type, validation, value, defVal } = this.props.setting; let newUnsavedValue; switch (type) { case 'boolean': const { unsavedChanges } = this.props; const currentValue = unsavedChanges ? unsavedChanges.value : getEditableValue(type, value, defVal); newUnsavedValue = !currentValue; break; case 'number': newUnsavedValue = Number(targetValue); break; default: newUnsavedValue = targetValue; } let errorParams = {}; if ((validation as StringValidationRegex)?.regex) { if (!(validation as StringValidationRegex).regex!.test(newUnsavedValue.toString())) { errorParams = { error: (validation as StringValidationRegex).message, isInvalid: true, }; } } this.handleChange({ value: newUnsavedValue, ...errorParams, }); }; onImageChange = async (files: FileList | null) => { if (files == null) return; if (!files.length) { this.setState({ unsavedValue: null, }); return; } const file = files[0]; const { maxSize } = this.props.setting.validation as ImageValidation; try { let base64Image = ''; if (file instanceof File) { base64Image = (await this.getImageAsBase64(file)) as string; } let errorParams = {}; const isInvalid = !!(maxSize?.length && base64Image.length > maxSize.length); if (isInvalid) { errorParams = { isInvalid, error: i18n.translate('advancedSettings.field.imageTooLargeErrorMessage', { defaultMessage: 'Image is too large, maximum size is {maxSizeDescription}', values: { maxSizeDescription: maxSize.description, }, }), }; } this.handleChange({ changeImage: true, value: base64Image, ...errorParams, }); } catch (err) { this.props.toasts.addDanger( i18n.translate('advancedSettings.field.imageChangeErrorMessage', { defaultMessage: 'Image could not be saved', }) ); this.cancelChangeImage(); } }; async getImageAsBase64(file: Blob): Promise { const reader = new FileReader(); reader.readAsDataURL(file); return new Promise((resolve, reject) => { reader.onload = () => { resolve(reader.result || undefined); }; reader.onerror = (err) => { reject(err); }; }); } changeImage = () => { this.handleChange({ value: null, changeImage: true, }); }; cancelChangeImage = () => { if (this.changeImageForm.current?.fileInput) { this.changeImageForm.current.fileInput.value = ''; this.changeImageForm.current.handleChange(); } if (this.props.clearChange) { this.props.clearChange(this.props.setting.name); } }; renderField(setting: FieldSetting, ariaDescribedBy?: string) { const { enableSaving, unsavedChanges, loading } = this.props; const { name, value, type, options, optionLabels = {}, isOverridden, defVal, ariaName, } = setting; const a11yProps: { [key: string]: string } = ariaDescribedBy ? { 'aria-label': ariaName, 'aria-describedby': ariaDescribedBy, } : { 'aria-label': ariaName, }; const currentValue = unsavedChanges ? unsavedChanges.value : getEditableValue(type, value, defVal); switch (type) { case 'boolean': return ( ) : ( ) } checked={!!currentValue} onChange={this.onFieldChangeSwitch} disabled={loading || isOverridden || !enableSaving} data-test-subj={`advancedSetting-editField-${name}`} {...a11yProps} /> ); case 'markdown': case 'json': return (
); case 'image': const changeImage = unsavedChanges?.changeImage; if (!isDefaultValue(setting) && !changeImage) { return ; } else { return ( ); } case 'select': return ( { return { text: optionLabels.hasOwnProperty(option) ? optionLabels[option] : option, value: option, }; })} onChange={this.onFieldChangeEvent} isLoading={loading} disabled={loading || isOverridden || !enableSaving} fullWidth data-test-subj={`advancedSetting-editField-${name}`} /> ); case 'number': return ( ); default: return ( ); } } renderLabel(setting: FieldSetting) { return setting.name; } renderHelpText(setting: FieldSetting) { if (setting.isOverridden) { return ( ); } const canUpdateSetting = this.props.enableSaving; const defaultLink = this.renderResetToDefaultLink(setting); const imageLink = this.renderChangeImageLink(setting); if (canUpdateSetting && (defaultLink || imageLink)) { return ( {defaultLink} {imageLink} ); } return null; } renderTitle(setting: FieldSetting) { const { unsavedChanges } = this.props; const isInvalid = unsavedChanges?.isInvalid; const unsavedIconLabel = unsavedChanges ? isInvalid ? i18n.translate('advancedSettings.field.invalidIconLabel', { defaultMessage: 'Invalid', }) : i18n.translate('advancedSettings.field.unsavedIconLabel', { defaultMessage: 'Unsaved', }) : undefined; return (

{setting.displayName || setting.name} {setting.isCustom ? ( } /> ) : ( '' )} {unsavedChanges ? ( ) : ( '' )}

); } renderDescription(setting: FieldSetting) { let description; let deprecation; if (setting.deprecation) { const links = this.props.dockLinks; deprecation = ( <> { window.open( links.noDocumentation.management[setting.deprecation!.docLinksKey], '_blank' ); }} onClickAriaLabel={i18n.translate('advancedSettings.field.deprecationClickAreaLabel', { defaultMessage: 'Click to view deprecation documentation for {settingName}.', values: { settingName: setting.name, }, })} > Deprecated ); } if (React.isValidElement(setting.description)) { description = setting.description; } else { description = (
); } return ( {deprecation} {description} {this.renderDefaultValue(setting)} ); } renderDefaultValue(setting: FieldSetting) { const { type, defVal, optionLabels } = setting; if (isDefaultValue(setting)) { return; } return ( {type === 'json' ? ( = 500 ? 300 : undefined} > {this.getDisplayedDefaultValue(type, defVal)} ), }} /> ) : ( {this.getDisplayedDefaultValue(type, defVal, optionLabels)} ), }} /> )} ); } renderResetToDefaultLink(setting: FieldSetting) { const { defVal, ariaName, name } = setting; if ( defVal === this.props.unsavedChanges?.value || isDefaultValue(setting) || this.props.loading ) { return; } return (     ); } renderChangeImageLink(setting: FieldSetting) { const changeImage = this.props.unsavedChanges?.changeImage; const { type, value, ariaName, name } = setting; if (type !== 'image' || !value || changeImage) { return; } return ( ); } render() { const { setting, unsavedChanges } = this.props; const error = unsavedChanges?.error; const isInvalid = unsavedChanges?.isInvalid; const className = classNames('mgtAdvancedSettings__field', { // eslint-disable-next-line @typescript-eslint/naming-convention 'mgtAdvancedSettings__field--unsaved': unsavedChanges, // eslint-disable-next-line @typescript-eslint/naming-convention 'mgtAdvancedSettings__field--invalid': isInvalid, }); const groupId = `${setting.name}-group`; const unsavedId = `${setting.name}-unsaved`; return ( <> {this.renderField(setting, unsavedChanges ? `${groupId} ${unsavedId}` : undefined)} {unsavedChanges && (

{unsavedChanges.error ? unsavedChanges.error : i18n.translate('advancedSettings.field.settingIsUnsaved', { defaultMessage: 'Setting is currently not saved.', })}

)}
); } }