|
@ -29,6 +29,7 @@
|
|||
"maps_legacy": "src/plugins/maps_legacy",
|
||||
"monaco": "packages/kbn-monaco/src",
|
||||
"presentationUtil": "src/plugins/presentation_util",
|
||||
"indexPatternFieldEditor": "src/plugins/index_pattern_field_editor",
|
||||
"indexPatternManagement": "src/plugins/index_pattern_management",
|
||||
"advancedSettings": "src/plugins/advanced_settings",
|
||||
"kibana_legacy": "src/plugins/kibana_legacy",
|
||||
|
|
|
@ -93,6 +93,10 @@ for use in their own application.
|
|||
|Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/index_pattern_field_editor/README.md[indexPatternFieldEditor]
|
||||
|The reusable field editor across Kibana!
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement]
|
||||
|WARNING: Missing README.
|
||||
|
||||
|
|
|
@ -18,7 +18,12 @@ const toDiagnostics = (error: PainlessError): monaco.editor.IMarkerData => {
|
|||
};
|
||||
};
|
||||
|
||||
export interface SyntaxErrors {
|
||||
[modelId: string]: PainlessError[];
|
||||
}
|
||||
export class DiagnosticsAdapter {
|
||||
private errors: SyntaxErrors = {};
|
||||
|
||||
constructor(private worker: WorkerAccessor) {
|
||||
const onModelAdd = (model: monaco.editor.IModel): void => {
|
||||
let handle: any;
|
||||
|
@ -55,8 +60,16 @@ export class DiagnosticsAdapter {
|
|||
|
||||
if (errorMarkers) {
|
||||
const model = monaco.editor.getModel(resource);
|
||||
this.errors = {
|
||||
...this.errors,
|
||||
[model!.id]: errorMarkers,
|
||||
};
|
||||
// Set the error markers and underline them with "Error" severity
|
||||
monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics));
|
||||
}
|
||||
}
|
||||
|
||||
public getSyntaxErrors() {
|
||||
return this.errors;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,14 @@
|
|||
|
||||
import { ID } from './constants';
|
||||
import { lexerRules, languageConfiguration } from './lexer_rules';
|
||||
import { getSuggestionProvider } from './language';
|
||||
import { getSuggestionProvider, getSyntaxErrors } from './language';
|
||||
|
||||
export const PainlessLang = { ID, getSuggestionProvider, lexerRules, languageConfiguration };
|
||||
export const PainlessLang = {
|
||||
ID,
|
||||
getSuggestionProvider,
|
||||
lexerRules,
|
||||
languageConfiguration,
|
||||
getSyntaxErrors,
|
||||
};
|
||||
|
||||
export { PainlessContext, PainlessAutocompleteField } from './types';
|
||||
|
|
|
@ -13,7 +13,7 @@ import { ID } from './constants';
|
|||
import { PainlessContext, PainlessAutocompleteField } from './types';
|
||||
import { PainlessWorker } from './worker';
|
||||
import { PainlessCompletionAdapter } from './completion_adapter';
|
||||
import { DiagnosticsAdapter } from './diagnostics_adapter';
|
||||
import { DiagnosticsAdapter, SyntaxErrors } from './diagnostics_adapter';
|
||||
|
||||
const workerProxyService = new WorkerProxyService();
|
||||
const editorStateService = new EditorStateService();
|
||||
|
@ -33,8 +33,15 @@ export const getSuggestionProvider = (
|
|||
return new PainlessCompletionAdapter(worker, editorStateService);
|
||||
};
|
||||
|
||||
let diagnosticsAdapter: DiagnosticsAdapter;
|
||||
|
||||
// Returns syntax errors for all models by model id
|
||||
export const getSyntaxErrors = (): SyntaxErrors => {
|
||||
return diagnosticsAdapter.getSyntaxErrors();
|
||||
};
|
||||
|
||||
monaco.languages.onLanguage(ID, async () => {
|
||||
workerProxyService.setup();
|
||||
|
||||
new DiagnosticsAdapter(worker);
|
||||
diagnosticsAdapter = new DiagnosticsAdapter(worker);
|
||||
});
|
||||
|
|
|
@ -33,7 +33,7 @@ pageLoadAssetSize:
|
|||
home: 41661
|
||||
indexLifecycleManagement: 107090
|
||||
indexManagement: 140608
|
||||
indexPatternManagement: 154222
|
||||
indexPatternManagement: 28222
|
||||
infra: 204800
|
||||
fleet: 415829
|
||||
ingestPipelines: 58003
|
||||
|
@ -103,6 +103,7 @@ pageLoadAssetSize:
|
|||
stackAlerts: 29684
|
||||
presentationUtil: 28545
|
||||
spacesOss: 18817
|
||||
indexPatternFieldEditor: 90489
|
||||
osquery: 107090
|
||||
fileUpload: 25664
|
||||
banners: 17946
|
||||
|
|
|
@ -121,6 +121,7 @@ export class DocLinksService {
|
|||
indexPatterns: {
|
||||
loadingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/tutorial-load-dataset.html`,
|
||||
introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`,
|
||||
fieldFormattersString: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/field-formatters-string.html`,
|
||||
},
|
||||
addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`,
|
||||
kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`,
|
||||
|
|
9
src/plugins/data/common/index_patterns/constants.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const;
|
|
@ -14,7 +14,7 @@ Object {
|
|||
},
|
||||
"count": 1,
|
||||
"esTypes": Array [
|
||||
"text",
|
||||
"keyword",
|
||||
],
|
||||
"lang": "lang",
|
||||
"name": "name",
|
||||
|
@ -49,7 +49,7 @@ Object {
|
|||
"count": 1,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"text",
|
||||
"keyword",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "number",
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('Field', function () {
|
|||
script: 'script',
|
||||
lang: 'lang',
|
||||
count: 1,
|
||||
esTypes: ['text'],
|
||||
esTypes: ['text'], // note, this will get replaced by the runtime field type
|
||||
aggregatable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
|
@ -71,7 +71,7 @@ describe('Field', function () {
|
|||
});
|
||||
|
||||
it('sets type field when _source field', () => {
|
||||
const field = getField({ name: '_source' });
|
||||
const field = getField({ name: '_source', runtimeField: undefined });
|
||||
expect(field.type).toEqual('_source');
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import type { RuntimeField } from '../types';
|
||||
import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types';
|
||||
import { KbnFieldType, getKbnFieldType, castEsToKbnFieldTypeName } from '../../kbn_field_types';
|
||||
import { KBN_FIELD_TYPES } from '../../kbn_field_types/types';
|
||||
import type { IFieldType } from './types';
|
||||
import { FieldSpec, IndexPattern } from '../..';
|
||||
|
@ -99,11 +99,13 @@ export class IndexPatternField implements IFieldType {
|
|||
}
|
||||
|
||||
public get type() {
|
||||
return this.spec.type;
|
||||
return this.runtimeField?.type
|
||||
? castEsToKbnFieldTypeName(this.runtimeField?.type)
|
||||
: this.spec.type;
|
||||
}
|
||||
|
||||
public get esTypes() {
|
||||
return this.spec.esTypes;
|
||||
return this.runtimeField?.type ? [this.runtimeField?.type] : this.spec.esTypes;
|
||||
}
|
||||
|
||||
public get scripted() {
|
||||
|
|
|
@ -12,3 +12,4 @@ export { IndexPatternsService, IndexPatternsContract } from './index_patterns';
|
|||
export type { IndexPattern } from './index_patterns';
|
||||
export * from './errors';
|
||||
export * from './expressions';
|
||||
export * from './constants';
|
||||
|
|
|
@ -565,7 +565,9 @@ Object {
|
|||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": undefined,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "number",
|
||||
"params": Object {
|
||||
|
@ -587,7 +589,7 @@ Object {
|
|||
"searchable": false,
|
||||
"shortDotsEnable": false,
|
||||
"subType": undefined,
|
||||
"type": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
"script date": Object {
|
||||
"aggregatable": true,
|
||||
|
|
|
@ -389,6 +389,8 @@ export class IndexPattern implements IIndexPattern {
|
|||
existingField.runtimeField = undefined;
|
||||
} else {
|
||||
// runtimeField only
|
||||
this.setFieldCustomLabel(name, null);
|
||||
this.deleteFieldFormat(name);
|
||||
this.fields.remove(existingField);
|
||||
}
|
||||
}
|
||||
|
@ -423,7 +425,6 @@ export class IndexPattern implements IIndexPattern {
|
|||
|
||||
if (fieldObject) {
|
||||
fieldObject.customLabel = newCustomLabel;
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFieldAttrs(fieldName, 'customLabel', newCustomLabel);
|
||||
|
|
|
@ -415,11 +415,10 @@ export class IndexPatternsService {
|
|||
},
|
||||
spec.fieldAttrs
|
||||
);
|
||||
// APPLY RUNTIME FIELDS
|
||||
// CREATE RUNTIME FIELDS
|
||||
for (const [key, value] of Object.entries(runtimeFieldMap || {})) {
|
||||
if (spec.fields[key]) {
|
||||
spec.fields[key].runtimeField = value;
|
||||
} else {
|
||||
// do not create runtime field if mapped field exists
|
||||
if (!spec.fields[key]) {
|
||||
spec.fields[key] = {
|
||||
name: key,
|
||||
type: castEsToKbnFieldTypeName(value.type),
|
||||
|
|
|
@ -10,15 +10,16 @@ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notificatio
|
|||
// eslint-disable-next-line
|
||||
import type { SavedObject } from 'src/core/server';
|
||||
import { IFieldType } from './fields';
|
||||
import { RUNTIME_FIELD_TYPES } from './constants';
|
||||
import { SerializedFieldFormat } from '../../../expressions/common';
|
||||
import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..';
|
||||
|
||||
export type FieldFormatMap = Record<string, SerializedFieldFormat>;
|
||||
const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const;
|
||||
type RuntimeType = typeof RUNTIME_FIELD_TYPES[number];
|
||||
|
||||
export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number];
|
||||
export interface RuntimeField {
|
||||
type: RuntimeType;
|
||||
script: {
|
||||
script?: {
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -211,11 +211,12 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
|||
// ----------------------------------
|
||||
const addField: FormHook<T, I>['__addField'] = useCallback(
|
||||
(field) => {
|
||||
const fieldExists = fieldsRefs.current[field.path] !== undefined;
|
||||
fieldsRefs.current[field.path] = field;
|
||||
|
||||
updateFormDataAt(field.path, field.value);
|
||||
|
||||
if (!field.isValidated) {
|
||||
if (!fieldExists && !field.isValidated) {
|
||||
setIsValid(undefined);
|
||||
|
||||
// When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**.
|
||||
|
|
69
src/plugins/index_pattern_field_editor/README.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
# Index pattern field editor
|
||||
|
||||
The reusable field editor across Kibana!
|
||||
|
||||
This editor can be used to
|
||||
|
||||
* create or edit a runtime field inside an index pattern.
|
||||
* edit concrete (mapped) fields. In this case certain functionalities will be disabled like the possibility to change the field _type_ or to set the field _value_.
|
||||
|
||||
## How to use
|
||||
|
||||
You first need to add in your kibana.json the "`indexPatternFieldEditor`" plugin as a required dependency of your plugin.
|
||||
|
||||
You will then receive in the start contract of the indexPatternFieldEditor plugin the following API:
|
||||
|
||||
### `openEditor(options: OpenFieldEditorOptions): CloseEditor`
|
||||
|
||||
Use this method to open the index pattern field editor to either create (runtime) or edit (concrete | runtime) a field.
|
||||
|
||||
#### `options`
|
||||
|
||||
`ctx: FieldEditorContext` (**required**)
|
||||
|
||||
This is the only required option. You need to provide the context in which the editor is being consumed. This object has the following properties:
|
||||
|
||||
- `indexPattern: IndexPattern`: the index pattern you want to create/edit the field into.
|
||||
|
||||
`onSave(field: IndexPatternField): void` (optional)
|
||||
|
||||
You can provide an optional `onSave` handler to be notified when the field has being created/updated. This handler is called after the field has been persisted to the saved object.
|
||||
|
||||
`fieldName: string` (optional)
|
||||
|
||||
You can optionally pass the name of a field to edit. Leave empty to create a new runtime field based field.
|
||||
|
||||
### `userPermissions.editIndexPattern(): boolean`
|
||||
|
||||
Convenience method that uses the `core.application.capabilities` api to determine whether the user can edit the index pattern.
|
||||
|
||||
### `<DeleteRuntimeFieldProvider />`
|
||||
|
||||
This children func React component provides a handler to delete one or multiple runtime fields.
|
||||
|
||||
#### Props
|
||||
|
||||
* `indexPattern: IndexPattern`: the current index pattern. (**required**)
|
||||
|
||||
```js
|
||||
|
||||
const { DeleteRuntimeFieldProvider } = indexPatternFieldEditor;
|
||||
|
||||
// Single field
|
||||
<DeleteRuntimeFieldProvider indexPattern={indexPattern}>
|
||||
{(deleteField) => (
|
||||
<EuiButton fill color="danger" onClick={() => deleteField('myField')}>
|
||||
Delete
|
||||
</EuiButton>
|
||||
)}
|
||||
</DeleteRuntimeFieldProvider>
|
||||
|
||||
// Multiple fields
|
||||
<DeleteRuntimeFieldProvider indexPattern={indexPattern}>
|
||||
{(deleteFields) => (
|
||||
<EuiButton fill color="danger" onClick={() => deleteFields(['field1', 'field2', 'field3'])}>
|
||||
Delete
|
||||
</EuiButton>
|
||||
)}
|
||||
</DeleteRuntimeFieldProvider>
|
||||
```
|
13
src/plugins/index_pattern_field_editor/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/src/plugins/index_pattern_field_editor'],
|
||||
};
|
9
src/plugins/index_pattern_field_editor/kibana.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "indexPatternFieldEditor",
|
||||
"version": "kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["data"],
|
||||
"optionalPlugins": ["usageCollection"],
|
||||
"requiredBundles": ["kibanaReact", "esUiShared", "usageCollection"]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Steven Skelton
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
After Width: | Height: | Size: 802 B |
After Width: | Height: | Size: 124 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 336 B |
After Width: | Height: | Size: 919 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1 KiB |
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
|
||||
|
||||
type DeleteFieldFunc = (fieldName: string | string[]) => void;
|
||||
|
||||
export interface Props {
|
||||
children: (deleteFieldHandler: DeleteFieldFunc) => React.ReactNode;
|
||||
onConfirmDelete: (fieldsToDelete: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isModalOpen: boolean;
|
||||
fieldsToDelete: string[];
|
||||
}
|
||||
|
||||
const geti18nTexts = (fieldsToDelete?: string[]) => {
|
||||
let modalTitle = '';
|
||||
if (fieldsToDelete) {
|
||||
const isSingle = fieldsToDelete.length === 1;
|
||||
|
||||
modalTitle = isSingle
|
||||
? i18n.translate(
|
||||
'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteSingleTitle',
|
||||
{
|
||||
defaultMessage: `Remove field '{name}'?`,
|
||||
values: { name: fieldsToDelete[0] },
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteMultipleTitle',
|
||||
{
|
||||
defaultMessage: `Remove {count} fields?`,
|
||||
values: { count: fieldsToDelete.length },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
modalTitle,
|
||||
confirmButtonText: i18n.translate(
|
||||
'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.removeButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Remove',
|
||||
}
|
||||
),
|
||||
cancelButtonText: i18n.translate(
|
||||
'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.cancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
),
|
||||
warningMultipleFields: i18n.translate(
|
||||
'indexPatternFieldEditor.deleteRuntimeField.confirmModal.multipleDeletionDescription',
|
||||
{
|
||||
defaultMessage: 'You are about to remove these runtime fields:',
|
||||
}
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const DeleteRuntimeFieldProvider = ({ children, onConfirmDelete }: Props) => {
|
||||
const [state, setState] = useState<State>({ isModalOpen: false, fieldsToDelete: [] });
|
||||
|
||||
const { isModalOpen, fieldsToDelete } = state;
|
||||
const i18nTexts = geti18nTexts(fieldsToDelete);
|
||||
const { modalTitle, confirmButtonText, cancelButtonText, warningMultipleFields } = i18nTexts;
|
||||
const isMultiple = Boolean(fieldsToDelete.length > 1);
|
||||
|
||||
const deleteField: DeleteFieldFunc = useCallback((fieldNames) => {
|
||||
setState({
|
||||
isModalOpen: true,
|
||||
fieldsToDelete: Array.isArray(fieldNames) ? fieldNames : [fieldNames],
|
||||
});
|
||||
}, []);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setState({ isModalOpen: false, fieldsToDelete: [] });
|
||||
}, []);
|
||||
|
||||
const confirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await onConfirmDelete(fieldsToDelete);
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
// silently fail as "onConfirmDelete" is responsible
|
||||
// to show a toast message if there is an error
|
||||
}
|
||||
}, [closeModal, onConfirmDelete, fieldsToDelete]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children(deleteField)}
|
||||
|
||||
{isModalOpen && (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={modalTitle}
|
||||
data-test-subj="runtimeFieldDeleteConfirmModal"
|
||||
onCancel={closeModal}
|
||||
onConfirm={confirmDelete}
|
||||
cancelButtonText={cancelButtonText}
|
||||
buttonColor="danger"
|
||||
confirmButtonText={confirmButtonText}
|
||||
>
|
||||
{isMultiple && (
|
||||
<>
|
||||
<p>{warningMultipleFields}</p>
|
||||
<ul>
|
||||
{fieldsToDelete.map((fieldName) => (
|
||||
<li key={fieldName}>{fieldName}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { NotificationsStart } from 'src/core/public';
|
||||
import { IndexPattern, UsageCollectionStart } from '../../shared_imports';
|
||||
import { pluginName } from '../../constants';
|
||||
import { DeleteRuntimeFieldProvider, Props as DeleteProviderProps } from './delete_field_provider';
|
||||
import { DataPublicPluginStart } from '../../../../data/public';
|
||||
|
||||
export interface Props extends Omit<DeleteProviderProps, 'onConfirmDelete'> {
|
||||
indexPattern: IndexPattern;
|
||||
onDelete?: (fieldNames: string[]) => void;
|
||||
}
|
||||
|
||||
export const getDeleteProvider = (
|
||||
indexPatternService: DataPublicPluginStart['indexPatterns'],
|
||||
usageCollection: UsageCollectionStart,
|
||||
notifications: NotificationsStart
|
||||
): React.FunctionComponent<Props> => {
|
||||
return React.memo(({ indexPattern, children, onDelete }: Props) => {
|
||||
const deleteFields = useCallback(
|
||||
async (fieldNames: string[]) => {
|
||||
fieldNames.forEach((fieldName) => {
|
||||
indexPattern.removeRuntimeField(fieldName);
|
||||
});
|
||||
|
||||
try {
|
||||
usageCollection.reportUiCounter(
|
||||
pluginName,
|
||||
usageCollection.METRIC_TYPE.COUNT,
|
||||
'delete_runtime'
|
||||
);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
await indexPatternService.updateSavedObject(indexPattern);
|
||||
} catch (e) {
|
||||
const title = i18n.translate('indexPatternFieldEditor.save.deleteErrorTitle', {
|
||||
defaultMessage: 'Failed to save field removal',
|
||||
});
|
||||
notifications.toasts.addError(e, { title });
|
||||
}
|
||||
|
||||
if (onDelete) {
|
||||
onDelete(fieldNames);
|
||||
}
|
||||
},
|
||||
[onDelete, indexPattern]
|
||||
);
|
||||
|
||||
return <DeleteRuntimeFieldProvider children={children} onConfirmDelete={deleteFields} />;
|
||||
});
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { getDeleteProvider, Props as DeleteProviderProps } from './get_delete_provider';
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AdvancedParametersSection = ({ children }: Props) => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
|
||||
const toggleIsVisible = () => {
|
||||
setIsVisible(!isVisible);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty onClick={toggleIsVisible} flush="left" data-test-subj="toggleAdvancedSetting">
|
||||
{isVisible
|
||||
? i18n.translate('indexPatternFieldEditor.editor.form.advancedSettings.hideButtonLabel', {
|
||||
defaultMessage: 'Hide advanced settings',
|
||||
})
|
||||
: i18n.translate('indexPatternFieldEditor.editor.form.advancedSettings.showButtonLabel', {
|
||||
defaultMessage: 'Show advanced settings',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<div style={{ display: isVisible ? 'block' : 'none' }} data-test-subj="advancedSettings">
|
||||
<EuiSpacer size="m" />
|
||||
{/* We ned to wrap the children inside a "div" to have our css :first-child rule */}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { RuntimeType } from '../../shared_imports';
|
||||
|
||||
export const RUNTIME_FIELD_OPTIONS: Array<EuiComboBoxOptionOption<RuntimeType>> = [
|
||||
{
|
||||
label: 'Keyword',
|
||||
value: 'keyword',
|
||||
},
|
||||
{
|
||||
label: 'Long',
|
||||
value: 'long',
|
||||
},
|
||||
{
|
||||
label: 'Double',
|
||||
value: 'double',
|
||||
},
|
||||
{
|
||||
label: 'Date',
|
||||
value: 'date',
|
||||
},
|
||||
{
|
||||
label: 'IP',
|
||||
value: 'ip',
|
||||
},
|
||||
{
|
||||
label: 'Boolean',
|
||||
value: 'boolean',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import '../../test_utils/setup_environment';
|
||||
import { registerTestBed, TestBed, getCommonActions } from '../../test_utils';
|
||||
import { Field } from '../../types';
|
||||
import { FieldEditor, Props, FieldEditorFormState } from './field_editor';
|
||||
|
||||
const defaultProps: Props = {
|
||||
onChange: jest.fn(),
|
||||
links: {
|
||||
runtimePainless: 'https://elastic.co',
|
||||
},
|
||||
ctx: {
|
||||
existingConcreteFields: [],
|
||||
namesNotAllowed: [],
|
||||
fieldTypeToProcess: 'runtime',
|
||||
},
|
||||
indexPattern: { fields: [] } as any,
|
||||
fieldFormatEditors: {
|
||||
getAll: () => [],
|
||||
getById: () => undefined,
|
||||
},
|
||||
fieldFormats: {} as any,
|
||||
uiSettings: {} as any,
|
||||
syntaxError: {
|
||||
error: null,
|
||||
clear: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
const setup = (props?: Partial<Props>) => {
|
||||
const testBed = registerTestBed(FieldEditor, {
|
||||
memoryRouter: {
|
||||
wrapComponent: false,
|
||||
},
|
||||
})({ ...defaultProps, ...props }) as TestBed;
|
||||
|
||||
const actions = {
|
||||
...getCommonActions(testBed),
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions,
|
||||
};
|
||||
};
|
||||
|
||||
describe('<FieldEditor />', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
let testBed: TestBed & { actions: ReturnType<typeof getCommonActions> };
|
||||
let onChange: jest.Mock<Props['onChange']> = jest.fn();
|
||||
|
||||
const lastOnChangeCall = (): FieldEditorFormState[] =>
|
||||
onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||
|
||||
const getLastStateUpdate = () => lastOnChangeCall()[0];
|
||||
|
||||
const submitFormAndGetData = async (state: FieldEditorFormState) => {
|
||||
let formState:
|
||||
| {
|
||||
data: Field;
|
||||
isValid: boolean;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
let promise: ReturnType<FieldEditorFormState['submit']>;
|
||||
|
||||
await act(async () => {
|
||||
// We can't await for the promise here as the validation for the
|
||||
// "script" field has a setTimeout which is mocked by jest. If we await
|
||||
// we don't have the chance to call jest.advanceTimersByTime and thus the
|
||||
// test times out.
|
||||
promise = state.submit();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
// The painless syntax validation has a timeout set to 600ms
|
||||
// we give it a bit more time just to be on the safe side
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
promise.then((response) => {
|
||||
formState = response;
|
||||
});
|
||||
});
|
||||
|
||||
return formState!;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange = jest.fn();
|
||||
});
|
||||
|
||||
test('initial state should have "set custom label", "set value" and "set format" turned off', () => {
|
||||
testBed = setup();
|
||||
|
||||
['customLabel', 'value', 'format'].forEach((row) => {
|
||||
const testSubj = `${row}Row.toggle`;
|
||||
const toggle = testBed.find(testSubj);
|
||||
const isOn = toggle.props()['aria-checked'];
|
||||
|
||||
try {
|
||||
expect(isOn).toBe(false);
|
||||
} catch (e) {
|
||||
e.message = `"${row}" row toggle expected to be 'off' but was 'on'. \n${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should accept a defaultValue and onChange prop to forward the form state', async () => {
|
||||
const field = {
|
||||
name: 'foo',
|
||||
type: 'date',
|
||||
script: { source: 'emit("hello")' },
|
||||
};
|
||||
|
||||
testBed = setup({ onChange, field });
|
||||
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
|
||||
let lastState = getLastStateUpdate();
|
||||
expect(lastState.isValid).toBe(undefined);
|
||||
expect(lastState.isSubmitted).toBe(false);
|
||||
expect(lastState.submit).toBeDefined();
|
||||
|
||||
const { data: formData } = await submitFormAndGetData(lastState);
|
||||
expect(formData).toEqual(field);
|
||||
|
||||
// Make sure that both isValid and isSubmitted state are now "true"
|
||||
lastState = getLastStateUpdate();
|
||||
expect(lastState.isValid).toBe(true);
|
||||
expect(lastState.isSubmitted).toBe(true);
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
test('should accept an optional list of existing fields and prevent creating duplicates', async () => {
|
||||
const existingFields = ['myRuntimeField'];
|
||||
testBed = setup({
|
||||
onChange,
|
||||
ctx: {
|
||||
namesNotAllowed: existingFields,
|
||||
existingConcreteFields: [],
|
||||
fieldTypeToProcess: 'runtime',
|
||||
},
|
||||
});
|
||||
|
||||
const { form, component, actions } = testBed;
|
||||
|
||||
await act(async () => {
|
||||
actions.toggleFormRow('value');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
form.setInputValue('nameField.input', existingFields[0]);
|
||||
form.setInputValue('scriptField', 'echo("hello")');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(1000); // Make sure our debounced error message is in the DOM
|
||||
});
|
||||
|
||||
const lastState = getLastStateUpdate();
|
||||
await submitFormAndGetData(lastState);
|
||||
component.update();
|
||||
expect(getLastStateUpdate().isValid).toBe(false);
|
||||
expect(form.getErrorsMessages()).toEqual(['A field with this name already exists.']);
|
||||
});
|
||||
|
||||
test('should not count the default value as a duplicate', async () => {
|
||||
const existingRuntimeFieldNames = ['myRuntimeField'];
|
||||
const field: Field = {
|
||||
name: 'myRuntimeField',
|
||||
type: 'boolean',
|
||||
script: { source: 'emit("hello"' },
|
||||
};
|
||||
|
||||
testBed = setup({
|
||||
field,
|
||||
onChange,
|
||||
ctx: {
|
||||
namesNotAllowed: existingRuntimeFieldNames,
|
||||
existingConcreteFields: [],
|
||||
fieldTypeToProcess: 'runtime',
|
||||
},
|
||||
});
|
||||
|
||||
const { form, component } = testBed;
|
||||
const lastState = getLastStateUpdate();
|
||||
await submitFormAndGetData(lastState);
|
||||
component.update();
|
||||
expect(getLastStateUpdate().isValid).toBe(true);
|
||||
expect(form.getErrorsMessages()).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,286 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiCode,
|
||||
} from '@elastic/eui';
|
||||
import type { CoreStart } from 'src/core/public';
|
||||
|
||||
import {
|
||||
Form,
|
||||
useForm,
|
||||
FormHook,
|
||||
UseField,
|
||||
TextField,
|
||||
RuntimeType,
|
||||
IndexPattern,
|
||||
DataPublicPluginStart,
|
||||
} from '../../shared_imports';
|
||||
import { Field, InternalFieldType, PluginStart } from '../../types';
|
||||
|
||||
import { RUNTIME_FIELD_OPTIONS } from './constants';
|
||||
import { schema } from './form_schema';
|
||||
import { getNameFieldConfig } from './lib';
|
||||
import {
|
||||
TypeField,
|
||||
CustomLabelField,
|
||||
ScriptField,
|
||||
FormatField,
|
||||
PopularityField,
|
||||
ScriptSyntaxError,
|
||||
} from './form_fields';
|
||||
import { FormRow } from './form_row';
|
||||
import { AdvancedParametersSection } from './advanced_parameters_section';
|
||||
|
||||
export interface FieldEditorFormState {
|
||||
isValid: boolean | undefined;
|
||||
isSubmitted: boolean;
|
||||
submit: FormHook<Field>['submit'];
|
||||
}
|
||||
|
||||
export interface FieldFormInternal extends Omit<Field, 'type' | 'internalType'> {
|
||||
type: Array<EuiComboBoxOptionOption<RuntimeType>>;
|
||||
__meta__: {
|
||||
isCustomLabelVisible: boolean;
|
||||
isValueVisible: boolean;
|
||||
isFormatVisible: boolean;
|
||||
isPopularityVisible: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
/** Link URLs to our doc site */
|
||||
links: {
|
||||
runtimePainless: string;
|
||||
};
|
||||
/** Optional field to edit */
|
||||
field?: Field;
|
||||
/** Handler to receive state changes updates */
|
||||
onChange?: (state: FieldEditorFormState) => void;
|
||||
indexPattern: IndexPattern;
|
||||
fieldFormatEditors: PluginStart['fieldFormatEditors'];
|
||||
fieldFormats: DataPublicPluginStart['fieldFormats'];
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
/** Context object */
|
||||
ctx: {
|
||||
/** The internal field type we are dealing with (concrete|runtime)*/
|
||||
fieldTypeToProcess: InternalFieldType;
|
||||
/**
|
||||
* An array of field names not allowed.
|
||||
* e.g we probably don't want a user to give a name of an existing
|
||||
* runtime field (for that the user should edit the existing runtime field).
|
||||
*/
|
||||
namesNotAllowed: string[];
|
||||
/**
|
||||
* An array of existing concrete fields. If the user gives a name to the runtime
|
||||
* field that matches one of the concrete fields, a callout will be displayed
|
||||
* to indicate that this runtime field will shadow the concrete field.
|
||||
* It is also used to provide the list of field autocomplete suggestions to the code editor.
|
||||
*/
|
||||
existingConcreteFields: Array<{ name: string; type: string }>;
|
||||
};
|
||||
syntaxError: ScriptSyntaxError;
|
||||
}
|
||||
|
||||
const geti18nTexts = (): {
|
||||
[key: string]: { title: string; description: JSX.Element | string };
|
||||
} => ({
|
||||
customLabel: {
|
||||
title: i18n.translate('indexPatternFieldEditor.editor.form.customLabelTitle', {
|
||||
defaultMessage: 'Set custom label',
|
||||
}),
|
||||
description: i18n.translate('indexPatternFieldEditor.editor.form.customLabelDescription', {
|
||||
defaultMessage: `Create a label to display in place of the field name in Discover, Maps, and Visualize. Useful for shortening a long field name. Queries and filters use the original field name.`,
|
||||
}),
|
||||
},
|
||||
value: {
|
||||
title: i18n.translate('indexPatternFieldEditor.editor.form.valueTitle', {
|
||||
defaultMessage: 'Set value',
|
||||
}),
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id="indexPatternFieldEditor.editor.form.valueDescription"
|
||||
defaultMessage="Set a value for the field instead of retrieving it from the field with the same name in {source}."
|
||||
values={{
|
||||
source: <EuiCode>{'_source'}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
format: {
|
||||
title: i18n.translate('indexPatternFieldEditor.editor.form.formatTitle', {
|
||||
defaultMessage: 'Set format',
|
||||
}),
|
||||
description: i18n.translate('indexPatternFieldEditor.editor.form.formatDescription', {
|
||||
defaultMessage: `Set your preferred format for displaying the value. Changing the format can affect the value and prevent highlighting in Discover.`,
|
||||
}),
|
||||
},
|
||||
popularity: {
|
||||
title: i18n.translate('indexPatternFieldEditor.editor.form.popularityTitle', {
|
||||
defaultMessage: 'Set popularity',
|
||||
}),
|
||||
description: i18n.translate('indexPatternFieldEditor.editor.form.popularityDescription', {
|
||||
defaultMessage: `Adjust the popularity to make the field appear higher or lower in the fields list. By default, Discover orders fields from most selected to least selected.`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const formDeserializer = (field: Field): FieldFormInternal => {
|
||||
let fieldType: Array<EuiComboBoxOptionOption<RuntimeType>>;
|
||||
if (!field.type) {
|
||||
fieldType = [RUNTIME_FIELD_OPTIONS[0]];
|
||||
} else {
|
||||
const label = RUNTIME_FIELD_OPTIONS.find(({ value }) => value === field.type)?.label;
|
||||
fieldType = [{ label: label ?? field.type, value: field.type as RuntimeType }];
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
type: fieldType,
|
||||
__meta__: {
|
||||
isCustomLabelVisible: field.customLabel !== undefined,
|
||||
isValueVisible: field.script !== undefined,
|
||||
isFormatVisible: field.format !== undefined,
|
||||
isPopularityVisible: field.popularity !== undefined,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const formSerializer = (field: FieldFormInternal): Field => {
|
||||
const { __meta__, type, ...rest } = field;
|
||||
return {
|
||||
type: type[0].value!,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
const FieldEditorComponent = ({
|
||||
field,
|
||||
onChange,
|
||||
links,
|
||||
indexPattern,
|
||||
fieldFormatEditors,
|
||||
fieldFormats,
|
||||
uiSettings,
|
||||
syntaxError,
|
||||
ctx: { fieldTypeToProcess, namesNotAllowed, existingConcreteFields },
|
||||
}: Props) => {
|
||||
const { form } = useForm<Field, FieldFormInternal>({
|
||||
defaultValue: field,
|
||||
schema,
|
||||
deserializer: formDeserializer,
|
||||
serializer: formSerializer,
|
||||
});
|
||||
const { submit, isValid: isFormValid, isSubmitted } = form;
|
||||
|
||||
const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field);
|
||||
const i18nTexts = geti18nTexts();
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange({ isValid: isFormValid, isSubmitted, submit });
|
||||
}
|
||||
}, [onChange, isFormValid, isSubmitted, submit]);
|
||||
|
||||
return (
|
||||
<Form form={form} className="indexPatternFieldEditor__form">
|
||||
<EuiFlexGroup>
|
||||
{/* Name */}
|
||||
<EuiFlexItem>
|
||||
<UseField<string, Field>
|
||||
path="name"
|
||||
config={nameFieldConfig}
|
||||
component={TextField}
|
||||
data-test-subj="nameField"
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
disabled: fieldTypeToProcess === 'concrete',
|
||||
'aria-label': i18n.translate('indexPatternFieldEditor.editor.form.nameAriaLabel', {
|
||||
defaultMessage: 'Name field',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{/* Type */}
|
||||
<EuiFlexItem>
|
||||
<TypeField isDisabled={fieldTypeToProcess === 'concrete'} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
{/* Set custom label */}
|
||||
<FormRow
|
||||
title={i18nTexts.customLabel.title}
|
||||
description={i18nTexts.customLabel.description}
|
||||
formFieldPath="__meta__.isCustomLabelVisible"
|
||||
data-test-subj="customLabelRow"
|
||||
withDividerRule
|
||||
>
|
||||
<CustomLabelField />
|
||||
</FormRow>
|
||||
|
||||
{/* Set value */}
|
||||
{fieldTypeToProcess === 'runtime' && (
|
||||
<FormRow
|
||||
title={i18nTexts.value.title}
|
||||
description={i18nTexts.value.description}
|
||||
formFieldPath="__meta__.isValueVisible"
|
||||
data-test-subj="valueRow"
|
||||
withDividerRule
|
||||
>
|
||||
<ScriptField
|
||||
existingConcreteFields={existingConcreteFields}
|
||||
links={links}
|
||||
syntaxError={syntaxError}
|
||||
/>
|
||||
</FormRow>
|
||||
)}
|
||||
|
||||
{/* Set custom format */}
|
||||
<FormRow
|
||||
title={i18nTexts.format.title}
|
||||
description={i18nTexts.format.description}
|
||||
formFieldPath="__meta__.isFormatVisible"
|
||||
data-test-subj="formatRow"
|
||||
withDividerRule
|
||||
>
|
||||
<FormatField
|
||||
indexPattern={indexPattern}
|
||||
fieldFormatEditors={fieldFormatEditors}
|
||||
fieldFormats={fieldFormats}
|
||||
uiSettings={uiSettings}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
{/* Advanced settings */}
|
||||
<AdvancedParametersSection>
|
||||
<FormRow
|
||||
title={i18nTexts.popularity.title}
|
||||
description={i18nTexts.popularity.description}
|
||||
formFieldPath="__meta__.isPopularityVisible"
|
||||
data-test-subj="popularityRow"
|
||||
withDividerRule
|
||||
>
|
||||
<PopularityField />
|
||||
</FormRow>
|
||||
</AdvancedParametersSection>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const FieldEditor = React.memo(FieldEditorComponent);
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { UseField, TextField } from '../../../shared_imports';
|
||||
|
||||
export const CustomLabelField = () => {
|
||||
return <UseField path="customLabel" component={TextField} />;
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { UseField, useFormData, ES_FIELD_TYPES, useFormContext } from '../../../shared_imports';
|
||||
import { FormatSelectEditor, FormatSelectEditorProps } from '../../field_format_editor';
|
||||
import { FieldFormInternal } from '../field_editor';
|
||||
import { FieldFormatConfig } from '../../../types';
|
||||
|
||||
export const FormatField = ({
|
||||
indexPattern,
|
||||
fieldFormatEditors,
|
||||
fieldFormats,
|
||||
uiSettings,
|
||||
}: Omit<FormatSelectEditorProps, 'onChange' | 'onError' | 'esTypes'>) => {
|
||||
const isMounted = useRef(false);
|
||||
const [{ type }] = useFormData<FieldFormInternal>({ watch: ['name', 'type'] });
|
||||
const { getFields, isSubmitted } = useFormContext();
|
||||
const [formatError, setFormatError] = useState<string | undefined>();
|
||||
// convert from combobox type to values
|
||||
const typeValue = type.reduce((collector, item) => {
|
||||
if (item.value !== undefined) {
|
||||
collector.push(item.value as ES_FIELD_TYPES);
|
||||
}
|
||||
return collector;
|
||||
}, [] as ES_FIELD_TYPES[]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formatError === undefined) {
|
||||
getFields().format.setErrors([]);
|
||||
} else {
|
||||
getFields().format.setErrors([{ message: formatError }]);
|
||||
}
|
||||
}, [formatError, getFields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMounted.current) {
|
||||
getFields().format.reset();
|
||||
}
|
||||
isMounted.current = true;
|
||||
}, [type, getFields]);
|
||||
|
||||
return (
|
||||
<UseField<FieldFormatConfig | undefined> path="format">
|
||||
{({ setValue, errors, value }) => {
|
||||
return (
|
||||
<>
|
||||
{isSubmitted && errors.length > 0 && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={errors.map((err) => err.message)}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
data-test-subj="formFormatError"
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormatSelectEditor
|
||||
esTypes={typeValue || (['keyword'] as ES_FIELD_TYPES[])}
|
||||
indexPattern={indexPattern}
|
||||
fieldFormatEditors={fieldFormatEditors}
|
||||
fieldFormats={fieldFormats}
|
||||
uiSettings={uiSettings}
|
||||
onChange={setValue}
|
||||
onError={setFormatError}
|
||||
value={value}
|
||||
key={typeValue.join(', ')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { TypeField } from './type_field';
|
||||
|
||||
export { CustomLabelField } from './custom_label_field';
|
||||
|
||||
export { PopularityField } from './popularity_field';
|
||||
|
||||
export { ScriptField, ScriptSyntaxError } from './script_field';
|
||||
|
||||
export { FormatField } from './format_field';
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { UseField, NumericField } from '../../../shared_imports';
|
||||
|
||||
export const PopularityField = () => {
|
||||
return (
|
||||
<UseField
|
||||
path="popularity"
|
||||
component={NumericField}
|
||||
componentProps={{ euiFieldProps: { 'data-test-subj': 'editorFieldCount' } }}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFormRow, EuiLink, EuiCode, EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { PainlessLang, PainlessContext } from '@kbn/monaco';
|
||||
|
||||
import {
|
||||
UseField,
|
||||
useFormData,
|
||||
RuntimeType,
|
||||
FieldConfig,
|
||||
CodeEditor,
|
||||
} from '../../../shared_imports';
|
||||
import { RuntimeFieldPainlessError } from '../../../lib';
|
||||
import { schema } from '../form_schema';
|
||||
import type { FieldFormInternal } from '../field_editor';
|
||||
|
||||
interface Props {
|
||||
links: { runtimePainless: string };
|
||||
existingConcreteFields?: Array<{ name: string; type: string }>;
|
||||
syntaxError: ScriptSyntaxError;
|
||||
}
|
||||
|
||||
export interface ScriptSyntaxError {
|
||||
error: RuntimeFieldPainlessError | null;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessContext => {
|
||||
switch (runtimeType) {
|
||||
case 'keyword':
|
||||
return 'string_script_field_script_field';
|
||||
case 'long':
|
||||
return 'long_script_field_script_field';
|
||||
case 'double':
|
||||
return 'double_script_field_script_field';
|
||||
case 'date':
|
||||
return 'date_script_field';
|
||||
case 'ip':
|
||||
return 'ip_script_field_script_field';
|
||||
case 'boolean':
|
||||
return 'boolean_script_field_script_field';
|
||||
default:
|
||||
return 'string_script_field_script_field';
|
||||
}
|
||||
};
|
||||
|
||||
export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxError }: Props) => {
|
||||
const editorValidationTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const [painlessContext, setPainlessContext] = useState<PainlessContext>(
|
||||
mapReturnTypeToPainlessContext(schema.type.defaultValue[0].value!)
|
||||
);
|
||||
|
||||
const [editorId, setEditorId] = useState<string | undefined>();
|
||||
|
||||
const suggestionProvider = PainlessLang.getSuggestionProvider(
|
||||
painlessContext,
|
||||
existingConcreteFields
|
||||
);
|
||||
|
||||
const [{ type, script: { source } = { source: '' } }] = useFormData<FieldFormInternal>({
|
||||
watch: ['type', 'script.source'],
|
||||
});
|
||||
|
||||
const { clear: clearSyntaxError } = syntaxError;
|
||||
|
||||
const sourceFieldConfig: FieldConfig<string> = useMemo(() => {
|
||||
return {
|
||||
...schema.script.source,
|
||||
validations: [
|
||||
...schema.script.source.validations,
|
||||
{
|
||||
validator: () => {
|
||||
if (editorValidationTimeout.current) {
|
||||
clearTimeout(editorValidationTimeout.current);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// monaco waits 500ms before validating, so we also add a delay
|
||||
// before checking if there are any syntax errors
|
||||
editorValidationTimeout.current = setTimeout(() => {
|
||||
const painlessSyntaxErrors = PainlessLang.getSyntaxErrors();
|
||||
// It is possible for there to be more than one editor in a view,
|
||||
// so we need to get the syntax errors based on the editor (aka model) ID
|
||||
const editorHasSyntaxErrors = editorId && painlessSyntaxErrors[editorId].length > 0;
|
||||
|
||||
if (editorHasSyntaxErrors) {
|
||||
return resolve({
|
||||
message: i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.scriptEditorValidationMessage',
|
||||
{
|
||||
defaultMessage: 'Invalid Painless syntax.',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
resolve(undefined);
|
||||
}, 600);
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [editorId]);
|
||||
|
||||
useEffect(() => {
|
||||
setPainlessContext(mapReturnTypeToPainlessContext(type[0]!.value!));
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
// Whenever the source changes we clear potential syntax errors
|
||||
clearSyntaxError();
|
||||
}, [source, clearSyntaxError]);
|
||||
|
||||
return (
|
||||
<UseField<string> path="script.source" config={sourceFieldConfig}>
|
||||
{({ value, setValue, label, isValid, getErrorsMessages }) => {
|
||||
let errorMessage: string | null = '';
|
||||
if (syntaxError.error !== null) {
|
||||
errorMessage = syntaxError.error.reason ?? syntaxError.error.message;
|
||||
} else {
|
||||
errorMessage = getErrorsMessages();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={label}
|
||||
error={errorMessage}
|
||||
isInvalid={syntaxError.error !== null || !isValid}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="indexPatternFieldEditor.editor.form.source.scriptFieldHelpText"
|
||||
defaultMessage="Runtime fields without a script retrieve values from {source}. If the field doesn't exist in _source, a search request returns no value. {learnMoreLink}"
|
||||
values={{
|
||||
learnMoreLink: (
|
||||
<EuiLink
|
||||
href={links.runtimePainless}
|
||||
target="_blank"
|
||||
external
|
||||
data-test-subj="painlessSyntaxLearnMoreLink"
|
||||
>
|
||||
{i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.script.learnMoreLinkText',
|
||||
{
|
||||
defaultMessage: 'Learn about script syntax.',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
),
|
||||
source: <EuiCode>{'_source'}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<CodeEditor
|
||||
languageId={PainlessLang.ID}
|
||||
suggestionProvider={suggestionProvider}
|
||||
// 99% width allows the editor to resize horizontally. 100% prevents it from resizing.
|
||||
width="99%"
|
||||
height="300px"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
editorDidMount={(editor) => setEditorId(editor.getModel()?.id)}
|
||||
options={{
|
||||
fontSize: 12,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'indent',
|
||||
automaticLayout: true,
|
||||
suggest: {
|
||||
snippetsPreventQuickSuggestions: false,
|
||||
},
|
||||
}}
|
||||
data-test-subj="scriptField"
|
||||
aria-label={i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.scriptEditorAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Script editor',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{/* Help the user debug the error by showing where it failed in the script */}
|
||||
{syntaxError.error !== null && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.scriptEditor.debugErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Syntax error detail',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiCodeBlock
|
||||
// @ts-ignore
|
||||
whiteSpace="pre"
|
||||
>
|
||||
{syntaxError.error.scriptStack.join('\n')}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { UseField, RuntimeType } from '../../../shared_imports';
|
||||
import { RUNTIME_FIELD_OPTIONS } from '../constants';
|
||||
|
||||
interface Props {
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const TypeField = ({ isDisabled = false }: Props) => {
|
||||
return (
|
||||
<UseField<Array<EuiComboBoxOptionOption<RuntimeType>>> path="type">
|
||||
{({ label, value, setValue }) => {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow label={label} fullWidth>
|
||||
<EuiComboBox
|
||||
placeholder={i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.runtimeType.placeholderLabel',
|
||||
{
|
||||
defaultMessage: 'Select a type',
|
||||
}
|
||||
)}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={RUNTIME_FIELD_OPTIONS}
|
||||
selectedOptions={value}
|
||||
onChange={(newValue) => {
|
||||
if (newValue.length === 0) {
|
||||
// Don't allow clearing the type. One must always be selected
|
||||
return;
|
||||
}
|
||||
setValue(newValue);
|
||||
}}
|
||||
isClearable={false}
|
||||
isDisabled={isDisabled}
|
||||
data-test-subj="typeField"
|
||||
aria-label={i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.typeSelectAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Type select',
|
||||
}
|
||||
)}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { UseField, ToggleField, useFormData } from '../../shared_imports';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
formFieldPath: string;
|
||||
children: React.ReactNode;
|
||||
description?: string | JSX.Element;
|
||||
withDividerRule?: boolean;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
export const FormRow = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
formFieldPath,
|
||||
withDividerRule = false,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}: Props) => {
|
||||
const [formData] = useFormData({ watch: formFieldPath });
|
||||
const isContentVisible = Boolean(get(formData, formFieldPath));
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup data-test-subj={dataTestSubj ?? 'formRow'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UseField
|
||||
path={formFieldPath}
|
||||
component={ToggleField}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
label: title,
|
||||
showLabel: false,
|
||||
'data-test-subj': 'toggle',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<div>
|
||||
{/* Title */}
|
||||
<EuiTitle size="xs">
|
||||
<h3>{title}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
{/* Description */}
|
||||
<EuiText size="s" color="subdued">
|
||||
{description}
|
||||
</EuiText>
|
||||
|
||||
{/* Content */}
|
||||
{isContentVisible && (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{withDividerRule && <EuiHorizontalRule />}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { fieldValidators } from '../../shared_imports';
|
||||
|
||||
import { RUNTIME_FIELD_OPTIONS } from './constants';
|
||||
|
||||
const { emptyField, numberGreaterThanField } = fieldValidators;
|
||||
|
||||
export const schema = {
|
||||
name: {
|
||||
label: i18n.translate('indexPatternFieldEditor.editor.form.nameLabel', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.validations.nameIsRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage: 'A name is required.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
type: {
|
||||
label: i18n.translate('indexPatternFieldEditor.editor.form.runtimeTypeLabel', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
defaultValue: [RUNTIME_FIELD_OPTIONS[0]],
|
||||
},
|
||||
script: {
|
||||
source: {
|
||||
label: i18n.translate('indexPatternFieldEditor.editor.form.defineFieldLabel', {
|
||||
defaultMessage: 'Define script',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.validations.scriptIsRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage: 'A script is required to set the field value.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
customLabel: {
|
||||
label: i18n.translate('indexPatternFieldEditor.editor.form.customLabelLabel', {
|
||||
defaultMessage: 'Custom label',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.validations.customLabelIsRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Give a label to the field.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
popularity: {
|
||||
label: i18n.translate('indexPatternFieldEditor.editor.form.popularityLabel', {
|
||||
defaultMessage: 'Popularity',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.validations.popularityIsRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Give a popularity to the field.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
validator: numberGreaterThanField({
|
||||
than: 0,
|
||||
allowEquality: true,
|
||||
message: i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.validations.popularityGreaterThan0ErrorMessage',
|
||||
{
|
||||
defaultMessage: 'The popularity must be zero or greater.',
|
||||
}
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
__meta__: {
|
||||
isCustomLabelVisible: {
|
||||
defaultValue: false,
|
||||
},
|
||||
isValueVisible: {
|
||||
defaultValue: false,
|
||||
},
|
||||
isFormatVisible: {
|
||||
defaultValue: false,
|
||||
},
|
||||
isPopularityVisible: {
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { FieldEditor } from './field_editor';
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ValidationFunc, FieldConfig } from '../../shared_imports';
|
||||
import { Field } from '../../types';
|
||||
import { schema } from './form_schema';
|
||||
import { Props } from './field_editor';
|
||||
|
||||
const createNameNotAllowedValidator = (
|
||||
namesNotAllowed: string[]
|
||||
): ValidationFunc<{}, string, string> => ({ value }) => {
|
||||
if (namesNotAllowed.includes(value)) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage',
|
||||
{
|
||||
defaultMessage: 'A field with this name already exists.',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamically retrieve the config for the "name" field, adding
|
||||
* a validator to avoid duplicated runtime fields to be created.
|
||||
*
|
||||
* @param namesNotAllowed Array of names not allowed for the field "name"
|
||||
* @param field Initial value of the form
|
||||
*/
|
||||
export const getNameFieldConfig = (
|
||||
namesNotAllowed?: string[],
|
||||
field?: Props['field']
|
||||
): FieldConfig<string, Field> => {
|
||||
const nameFieldConfig = schema.name as FieldConfig<string, Field>;
|
||||
|
||||
if (!namesNotAllowed) {
|
||||
return nameFieldConfig;
|
||||
}
|
||||
|
||||
// Add validation to not allow duplicates
|
||||
return {
|
||||
...nameFieldConfig!,
|
||||
validations: [
|
||||
...(nameFieldConfig.validations ?? []),
|
||||
{
|
||||
validator: createNameNotAllowedValidator(
|
||||
namesNotAllowed.filter((name) => name !== field?.name)
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
export const ShadowingFieldWarning = () => {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('indexPatternFieldEditor.editor.form.fieldShadowingCalloutTitle', {
|
||||
defaultMessage: 'Field shadowing',
|
||||
})}
|
||||
color="warning"
|
||||
iconType="pin"
|
||||
size="s"
|
||||
data-test-subj="shadowingFieldCallout"
|
||||
>
|
||||
<div>
|
||||
{i18n.translate('indexPatternFieldEditor.editor.form.fieldShadowingCalloutDescription', {
|
||||
defaultMessage:
|
||||
'This field shares the name of a mapped field. Values for this field will be returned in search results.',
|
||||
})}
|
||||
</div>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import '../test_utils/setup_environment';
|
||||
import { registerTestBed, TestBed, noop, docLinks, getCommonActions } from '../test_utils';
|
||||
|
||||
import { FieldEditor } from './field_editor';
|
||||
import { FieldEditorFlyoutContent, Props } from './field_editor_flyout_content';
|
||||
|
||||
const defaultProps: Props = {
|
||||
onSave: noop,
|
||||
onCancel: noop,
|
||||
docLinks,
|
||||
FieldEditor,
|
||||
indexPattern: { fields: [] } as any,
|
||||
uiSettings: {} as any,
|
||||
fieldFormats: {} as any,
|
||||
fieldFormatEditors: {} as any,
|
||||
fieldTypeToProcess: 'runtime',
|
||||
runtimeFieldValidator: () => Promise.resolve(null),
|
||||
isSavingField: false,
|
||||
};
|
||||
|
||||
const setup = (props: Props = defaultProps) => {
|
||||
const testBed = registerTestBed(FieldEditorFlyoutContent, {
|
||||
memoryRouter: { wrapComponent: false },
|
||||
})(props) as TestBed;
|
||||
|
||||
const actions = {
|
||||
...getCommonActions(testBed),
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions,
|
||||
};
|
||||
};
|
||||
|
||||
describe('<FieldEditorFlyoutContent />', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('should have the correct title', () => {
|
||||
const { exists, find } = setup();
|
||||
expect(exists('flyoutTitle')).toBe(true);
|
||||
expect(find('flyoutTitle').text()).toBe('Create field');
|
||||
});
|
||||
|
||||
test('should allow a field to be provided', () => {
|
||||
const field = {
|
||||
name: 'foo',
|
||||
type: 'ip',
|
||||
script: {
|
||||
source: 'emit("hello world")',
|
||||
},
|
||||
};
|
||||
|
||||
const { find } = setup({ ...defaultProps, field });
|
||||
|
||||
expect(find('flyoutTitle').text()).toBe(`Edit ${field.name} field`);
|
||||
expect(find('nameField.input').props().value).toBe(field.name);
|
||||
expect(find('typeField').props().value).toBe(field.type);
|
||||
expect(find('scriptField').props().value).toBe(field.script.source);
|
||||
});
|
||||
|
||||
test('should accept an "onSave" prop', async () => {
|
||||
const field = {
|
||||
name: 'foo',
|
||||
type: 'date',
|
||||
script: { source: 'test=123' },
|
||||
};
|
||||
const onSave: jest.Mock<Props['onSave']> = jest.fn();
|
||||
|
||||
const { find } = setup({ ...defaultProps, onSave, field });
|
||||
|
||||
await act(async () => {
|
||||
find('fieldSaveButton').simulate('click');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
// The painless syntax validation has a timeout set to 600ms
|
||||
// we give it a bit more time just to be on the safe side
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0];
|
||||
expect(fieldReturned).toEqual(field);
|
||||
});
|
||||
|
||||
test('should accept an onCancel prop', () => {
|
||||
const onCancel = jest.fn();
|
||||
const { find } = setup({ ...defaultProps, onCancel });
|
||||
|
||||
find('closeFlyoutButton').simulate('click');
|
||||
|
||||
expect(onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
test('should validate the fields and prevent saving invalid form', async () => {
|
||||
const onSave: jest.Mock<Props['onSave']> = jest.fn();
|
||||
|
||||
const { find, exists, form, component } = setup({ ...defaultProps, onSave });
|
||||
|
||||
expect(find('fieldSaveButton').props().disabled).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
find('fieldSaveButton').simulate('click');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
expect(onSave).toHaveBeenCalledTimes(0);
|
||||
expect(find('fieldSaveButton').props().disabled).toBe(true);
|
||||
expect(form.getErrorsMessages()).toEqual(['A name is required.']);
|
||||
expect(exists('formError')).toBe(true);
|
||||
expect(find('formError').text()).toBe('Fix errors in form before continuing.');
|
||||
});
|
||||
|
||||
test('should forward values from the form', async () => {
|
||||
const onSave: jest.Mock<Props['onSave']> = jest.fn();
|
||||
|
||||
const {
|
||||
find,
|
||||
component,
|
||||
form,
|
||||
actions: { toggleFormRow },
|
||||
} = setup({ ...defaultProps, onSave });
|
||||
|
||||
act(() => {
|
||||
form.setInputValue('nameField.input', 'someName');
|
||||
toggleFormRow('value');
|
||||
});
|
||||
component.update();
|
||||
|
||||
await act(async () => {
|
||||
form.setInputValue('scriptField', 'echo("hello")');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
// Let's make sure that validation has finished running
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
find('fieldSaveButton').simulate('click');
|
||||
});
|
||||
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
|
||||
let fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0];
|
||||
|
||||
expect(fieldReturned).toEqual({
|
||||
name: 'someName',
|
||||
type: 'keyword', // default to keyword
|
||||
script: { source: 'echo("hello")' },
|
||||
});
|
||||
|
||||
// Change the type and make sure it is forwarded
|
||||
act(() => {
|
||||
find('typeField').simulate('change', [
|
||||
{
|
||||
label: 'Other type',
|
||||
value: 'other_type',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
find('fieldSaveButton').simulate('click');
|
||||
});
|
||||
|
||||
fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0];
|
||||
|
||||
expect(fieldReturned).toEqual({
|
||||
name: 'someName',
|
||||
type: 'other_type',
|
||||
script: { source: 'echo("hello")' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { DocLinksStart, CoreStart } from 'src/core/public';
|
||||
|
||||
import { Field, InternalFieldType, PluginStart, EsRuntimeField } from '../types';
|
||||
import { getLinks, RuntimeFieldPainlessError } from '../lib';
|
||||
import type { IndexPattern, DataPublicPluginStart } from '../shared_imports';
|
||||
import type { Props as FieldEditorProps, FieldEditorFormState } from './field_editor/field_editor';
|
||||
|
||||
const geti18nTexts = (field?: Field) => {
|
||||
return {
|
||||
flyoutTitle: field
|
||||
? i18n.translate('indexPatternFieldEditor.editor.flyoutEditFieldTitle', {
|
||||
defaultMessage: 'Edit {fieldName} field',
|
||||
values: {
|
||||
fieldName: field.name,
|
||||
},
|
||||
})
|
||||
: i18n.translate('indexPatternFieldEditor.editor.flyoutDefaultTitle', {
|
||||
defaultMessage: 'Create field',
|
||||
}),
|
||||
closeButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutCloseButtonLabel', {
|
||||
defaultMessage: 'Close',
|
||||
}),
|
||||
saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', {
|
||||
defaultMessage: 'Save',
|
||||
}),
|
||||
formErrorsCalloutTitle: i18n.translate('indexPatternFieldEditor.editor.validationErrorTitle', {
|
||||
defaultMessage: 'Fix errors in form before continuing.',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
/**
|
||||
* Handler for the "save" footer button
|
||||
*/
|
||||
onSave: (field: Field) => void;
|
||||
/**
|
||||
* Handler for the "cancel" footer button
|
||||
*/
|
||||
onCancel: () => void;
|
||||
/**
|
||||
* The docLinks start service from core
|
||||
*/
|
||||
docLinks: DocLinksStart;
|
||||
/**
|
||||
* The Field editor component that contains the form to create or edit a field
|
||||
*/
|
||||
FieldEditor: React.ComponentType<FieldEditorProps> | null;
|
||||
/** The internal field type we are dealing with (concrete|runtime)*/
|
||||
fieldTypeToProcess: InternalFieldType;
|
||||
/** Handler to validate the script */
|
||||
runtimeFieldValidator: (field: EsRuntimeField) => Promise<RuntimeFieldPainlessError | null>;
|
||||
/** Optional field to process */
|
||||
field?: Field;
|
||||
|
||||
indexPattern: IndexPattern;
|
||||
fieldFormatEditors: PluginStart['fieldFormatEditors'];
|
||||
fieldFormats: DataPublicPluginStart['fieldFormats'];
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
isSavingField: boolean;
|
||||
}
|
||||
|
||||
const FieldEditorFlyoutContentComponent = ({
|
||||
field,
|
||||
onSave,
|
||||
onCancel,
|
||||
FieldEditor,
|
||||
docLinks,
|
||||
indexPattern,
|
||||
fieldFormatEditors,
|
||||
fieldFormats,
|
||||
uiSettings,
|
||||
fieldTypeToProcess,
|
||||
runtimeFieldValidator,
|
||||
isSavingField,
|
||||
}: Props) => {
|
||||
const i18nTexts = geti18nTexts(field);
|
||||
|
||||
const [formState, setFormState] = useState<FieldEditorFormState>({
|
||||
isSubmitted: false,
|
||||
isValid: field ? true : undefined,
|
||||
submit: field
|
||||
? async () => ({ isValid: true, data: field })
|
||||
: async () => ({ isValid: false, data: {} as Field }),
|
||||
});
|
||||
|
||||
const [painlessSyntaxError, setPainlessSyntaxError] = useState<RuntimeFieldPainlessError | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
const { submit, isValid: isFormValid, isSubmitted } = formState;
|
||||
const { fields } = indexPattern;
|
||||
const isSaveButtonDisabled = isFormValid === false || painlessSyntaxError !== null;
|
||||
|
||||
const clearSyntaxError = useCallback(() => setPainlessSyntaxError(null), []);
|
||||
|
||||
const syntaxError = useMemo(
|
||||
() => ({
|
||||
error: painlessSyntaxError,
|
||||
clear: clearSyntaxError,
|
||||
}),
|
||||
[painlessSyntaxError, clearSyntaxError]
|
||||
);
|
||||
|
||||
const onClickSave = useCallback(async () => {
|
||||
const { isValid, data } = await submit();
|
||||
|
||||
if (isValid) {
|
||||
if (data.script) {
|
||||
setIsValidating(true);
|
||||
|
||||
const error = await runtimeFieldValidator({
|
||||
type: data.type,
|
||||
script: data.script,
|
||||
});
|
||||
|
||||
setIsValidating(false);
|
||||
setPainlessSyntaxError(error);
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSave(data);
|
||||
}
|
||||
}, [onSave, submit, runtimeFieldValidator]);
|
||||
|
||||
const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]);
|
||||
|
||||
const existingConcreteFields = useMemo(() => {
|
||||
const existing: Array<{ name: string; type: string }> = [];
|
||||
|
||||
fields
|
||||
.filter((fld) => {
|
||||
const isFieldBeingEdited = field?.name === fld.name;
|
||||
return !isFieldBeingEdited && fld.isMapped;
|
||||
})
|
||||
.forEach((fld) => {
|
||||
existing.push({
|
||||
name: fld.name,
|
||||
type: (fld.esTypes && fld.esTypes[0]) || '',
|
||||
});
|
||||
});
|
||||
|
||||
return existing;
|
||||
}, [fields, field]);
|
||||
|
||||
const ctx = useMemo(
|
||||
() => ({
|
||||
fieldTypeToProcess,
|
||||
namesNotAllowed,
|
||||
existingConcreteFields,
|
||||
}),
|
||||
[fieldTypeToProcess, namesNotAllowed, existingConcreteFields]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m" data-test-subj="flyoutTitle">
|
||||
<h2 id="fieldEditorTitle">{i18nTexts.flyoutTitle}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
{FieldEditor && (
|
||||
<FieldEditor
|
||||
indexPattern={indexPattern}
|
||||
fieldFormatEditors={fieldFormatEditors}
|
||||
fieldFormats={fieldFormats}
|
||||
uiSettings={uiSettings}
|
||||
links={getLinks(docLinks)}
|
||||
field={field}
|
||||
onChange={setFormState}
|
||||
ctx={ctx}
|
||||
syntaxError={syntaxError}
|
||||
/>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
{FieldEditor && (
|
||||
<>
|
||||
{isSubmitted && isSaveButtonDisabled && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18nTexts.formErrorsCalloutTitle}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
data-test-subj="formError"
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={onCancel}
|
||||
data-test-subj="closeFlyoutButton"
|
||||
>
|
||||
{i18nTexts.closeButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
onClick={onClickSave}
|
||||
data-test-subj="fieldSaveButton"
|
||||
fill
|
||||
disabled={isSaveButtonDisabled}
|
||||
isLoading={isSavingField || isValidating}
|
||||
>
|
||||
{i18nTexts.saveButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FieldEditorFlyoutContent = React.memo(FieldEditorFlyoutContentComponent);
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { DocLinksStart, NotificationsStart, CoreStart } from 'src/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
IndexPatternField,
|
||||
IndexPattern,
|
||||
DataPublicPluginStart,
|
||||
RuntimeType,
|
||||
UsageCollectionStart,
|
||||
} from '../shared_imports';
|
||||
import { Field, PluginStart, InternalFieldType } from '../types';
|
||||
import { pluginName } from '../constants';
|
||||
import { deserializeField, getRuntimeFieldValidator } from '../lib';
|
||||
import { Props as FieldEditorProps } from './field_editor/field_editor';
|
||||
import { FieldEditorFlyoutContent } from './field_editor_flyout_content';
|
||||
|
||||
export interface FieldEditorContext {
|
||||
indexPattern: IndexPattern;
|
||||
/**
|
||||
* The Kibana field type of the field to create or edit
|
||||
* Default: "runtime"
|
||||
*/
|
||||
fieldTypeToProcess: InternalFieldType;
|
||||
/** The search service from the data plugin */
|
||||
search: DataPublicPluginStart['search'];
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
/**
|
||||
* Handler for the "save" footer button
|
||||
*/
|
||||
onSave: (field: IndexPatternField) => void;
|
||||
/**
|
||||
* Handler for the "cancel" footer button
|
||||
*/
|
||||
onCancel: () => void;
|
||||
/**
|
||||
* The docLinks start service from core
|
||||
*/
|
||||
docLinks: DocLinksStart;
|
||||
/**
|
||||
* The context object specific to where the editor is currently being consumed
|
||||
*/
|
||||
ctx: FieldEditorContext;
|
||||
/**
|
||||
* Optional field to edit
|
||||
*/
|
||||
field?: IndexPatternField;
|
||||
/**
|
||||
* Services
|
||||
*/
|
||||
indexPatternService: DataPublicPluginStart['indexPatterns'];
|
||||
notifications: NotificationsStart;
|
||||
fieldFormatEditors: PluginStart['fieldFormatEditors'];
|
||||
fieldFormats: DataPublicPluginStart['fieldFormats'];
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
usageCollection: UsageCollectionStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* The container component will be in charge of the communication with the index pattern service
|
||||
* to retrieve/save the field in the saved object.
|
||||
* The <FieldEditorFlyoutContent /> component is the presentational component that won't know
|
||||
* anything about where a field comes from and where it should be persisted.
|
||||
*/
|
||||
|
||||
export const FieldEditorFlyoutContentContainer = ({
|
||||
field,
|
||||
onSave,
|
||||
onCancel,
|
||||
docLinks,
|
||||
indexPatternService,
|
||||
ctx: { indexPattern, fieldTypeToProcess, search },
|
||||
notifications,
|
||||
fieldFormatEditors,
|
||||
fieldFormats,
|
||||
uiSettings,
|
||||
usageCollection,
|
||||
}: Props) => {
|
||||
const fieldToEdit = deserializeField(indexPattern, field);
|
||||
const [Editor, setEditor] = useState<React.ComponentType<FieldEditorProps> | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const saveField = useCallback(
|
||||
async (updatedField: Field) => {
|
||||
setIsSaving(true);
|
||||
|
||||
const { script } = updatedField;
|
||||
|
||||
if (fieldTypeToProcess === 'runtime') {
|
||||
try {
|
||||
usageCollection.reportUiCounter(
|
||||
pluginName,
|
||||
usageCollection.METRIC_TYPE.COUNT,
|
||||
'save_runtime'
|
||||
);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
// rename an existing runtime field
|
||||
if (field?.name && field.name !== updatedField.name) {
|
||||
indexPattern.removeRuntimeField(field.name);
|
||||
}
|
||||
|
||||
indexPattern.addRuntimeField(updatedField.name, {
|
||||
type: updatedField.type as RuntimeType,
|
||||
script,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
usageCollection.reportUiCounter(
|
||||
pluginName,
|
||||
usageCollection.METRIC_TYPE.COUNT,
|
||||
'save_concrete'
|
||||
);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const editedField = indexPattern.getFieldByName(updatedField.name);
|
||||
|
||||
try {
|
||||
if (!editedField) {
|
||||
throw new Error(
|
||||
`Unable to find field named '${updatedField.name}' on index pattern '${indexPattern.title}'`
|
||||
);
|
||||
}
|
||||
|
||||
indexPattern.setFieldCustomLabel(updatedField.name, updatedField.customLabel);
|
||||
editedField.count = updatedField.popularity || 0;
|
||||
if (updatedField.format) {
|
||||
indexPattern.setFieldFormat(updatedField.name, updatedField.format);
|
||||
} else {
|
||||
indexPattern.deleteFieldFormat(updatedField.name);
|
||||
}
|
||||
|
||||
await indexPatternService.updateSavedObject(indexPattern).then(() => {
|
||||
const message = i18n.translate('indexPatternFieldEditor.deleteField.savedHeader', {
|
||||
defaultMessage: "Saved '{fieldName}'",
|
||||
values: { fieldName: updatedField.name },
|
||||
});
|
||||
notifications.toasts.addSuccess(message);
|
||||
setIsSaving(false);
|
||||
onSave(editedField);
|
||||
});
|
||||
} catch (e) {
|
||||
const title = i18n.translate('indexPatternFieldEditor.save.errorTitle', {
|
||||
defaultMessage: 'Failed to save field changes',
|
||||
});
|
||||
notifications.toasts.addError(e, { title });
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
onSave,
|
||||
indexPattern,
|
||||
indexPatternService,
|
||||
notifications,
|
||||
fieldTypeToProcess,
|
||||
field?.name,
|
||||
usageCollection,
|
||||
]
|
||||
);
|
||||
|
||||
const validateRuntimeField = useMemo(() => getRuntimeFieldValidator(indexPattern.title, search), [
|
||||
search,
|
||||
indexPattern,
|
||||
]);
|
||||
|
||||
const loadEditor = useCallback(async () => {
|
||||
const { FieldEditor } = await import('./field_editor');
|
||||
|
||||
setEditor(() => FieldEditor);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// On mount: load the editor asynchronously
|
||||
loadEditor();
|
||||
}, [loadEditor]);
|
||||
|
||||
return (
|
||||
<FieldEditorFlyoutContent
|
||||
onSave={saveField}
|
||||
onCancel={onCancel}
|
||||
docLinks={docLinks}
|
||||
field={fieldToEdit}
|
||||
FieldEditor={Editor}
|
||||
fieldFormatEditors={fieldFormatEditors}
|
||||
fieldFormats={fieldFormats}
|
||||
uiSettings={uiSettings}
|
||||
indexPattern={indexPattern}
|
||||
fieldTypeToProcess={fieldTypeToProcess}
|
||||
runtimeFieldValidator={validateRuntimeField}
|
||||
isSavingField={isSaving}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FieldFormatEditor should render normally 1`] = `
|
||||
<Fragment>
|
||||
<TestEditor
|
||||
fieldType="number"
|
||||
format={Object {}}
|
||||
formatParams={Object {}}
|
||||
onChange={[Function]}
|
||||
onError={[Function]}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`FieldFormatEditor should render nothing if there is no editor for the format 1`] = `
|
||||
<Fragment>
|
||||
<TestEditor
|
||||
fieldType="number"
|
||||
format={Object {}}
|
||||
formatParams={Object {}}
|
||||
onChange={[Function]}
|
||||
onError={[Function]}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -16,7 +16,7 @@ exports[`BytesFormatEditor should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Documentation"
|
||||
id="indexPatternManagement.number.documentationLabel"
|
||||
id="indexPatternFieldEditor.number.documentationLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
||||
|
@ -30,7 +30,7 @@ exports[`BytesFormatEditor should render normally 1`] = `
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
||||
id="indexPatternManagement.number.numeralLabel"
|
||||
id="indexPatternFieldEditor.number.numeralLabel"
|
||||
values={
|
||||
Object {
|
||||
"defaultPattern": <EuiCode>
|
|
@ -9,7 +9,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
|||
"field": "regex",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Pattern (regular expression)"
|
||||
id="indexPatternManagement.color.patternLabel"
|
||||
id="indexPatternFieldEditor.color.patternLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -18,7 +18,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
|||
"field": "text",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Text color"
|
||||
id="indexPatternManagement.color.textColorLabel"
|
||||
id="indexPatternFieldEditor.color.textColorLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -27,7 +27,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
|||
"field": "background",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Background color"
|
||||
id="indexPatternManagement.color.backgroundLabel"
|
||||
id="indexPatternFieldEditor.color.backgroundLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -35,7 +35,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
|||
Object {
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Example"
|
||||
id="indexPatternManagement.color.exampleLabel"
|
||||
id="indexPatternFieldEditor.color.exampleLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -89,7 +89,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add color"
|
||||
id="indexPatternManagement.color.addColorButton"
|
||||
id="indexPatternFieldEditor.color.addColorButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
|
@ -108,7 +108,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
|||
"field": "range",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Range (min:max)"
|
||||
id="indexPatternManagement.color.rangeLabel"
|
||||
id="indexPatternFieldEditor.color.rangeLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -117,7 +117,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
|||
"field": "text",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Text color"
|
||||
id="indexPatternManagement.color.textColorLabel"
|
||||
id="indexPatternFieldEditor.color.textColorLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -126,7 +126,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
|||
"field": "background",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Background color"
|
||||
id="indexPatternManagement.color.backgroundLabel"
|
||||
id="indexPatternFieldEditor.color.backgroundLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -134,7 +134,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
|||
Object {
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Example"
|
||||
id="indexPatternManagement.color.exampleLabel"
|
||||
id="indexPatternFieldEditor.color.exampleLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -181,7 +181,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add color"
|
||||
id="indexPatternManagement.color.addColorButton"
|
||||
id="indexPatternFieldEditor.color.addColorButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
|
@ -200,7 +200,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
|||
"field": "regex",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Pattern (regular expression)"
|
||||
id="indexPatternManagement.color.patternLabel"
|
||||
id="indexPatternFieldEditor.color.patternLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -209,7 +209,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
|||
"field": "text",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Text color"
|
||||
id="indexPatternManagement.color.textColorLabel"
|
||||
id="indexPatternFieldEditor.color.textColorLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -218,7 +218,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
|||
"field": "background",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Background color"
|
||||
id="indexPatternManagement.color.backgroundLabel"
|
||||
id="indexPatternFieldEditor.color.backgroundLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -226,7 +226,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
|||
Object {
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Example"
|
||||
id="indexPatternManagement.color.exampleLabel"
|
||||
id="indexPatternFieldEditor.color.exampleLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -273,7 +273,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add color"
|
||||
id="indexPatternManagement.color.addColorButton"
|
||||
id="indexPatternFieldEditor.color.addColorButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
|
@ -11,7 +11,7 @@ import { shallowWithI18nProvider } from '@kbn/test/jest';
|
|||
import { FieldFormat } from 'src/plugins/data/public';
|
||||
|
||||
import { ColorFormatEditor } from './color';
|
||||
import { fieldFormats } from '../../../../../../../../data/public';
|
||||
import { fieldFormats } from '../../../../../../data/public';
|
||||
|
||||
const fieldType = 'string';
|
||||
const format = {
|
|
@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { DefaultFormatEditor, FormatEditorProps } from '../default';
|
||||
|
||||
import { fieldFormats } from '../../../../../../../../../plugins/data/public';
|
||||
import { fieldFormats } from '../../../../../../data/public';
|
||||
|
||||
interface Color {
|
||||
range?: string;
|
||||
|
@ -86,7 +86,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
|||
field: 'regex',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.color.patternLabel"
|
||||
id="indexPatternFieldEditor.color.patternLabel"
|
||||
defaultMessage="Pattern (regular expression)"
|
||||
/>
|
||||
),
|
||||
|
@ -110,7 +110,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
|||
field: 'range',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.color.rangeLabel"
|
||||
id="indexPatternFieldEditor.color.rangeLabel"
|
||||
defaultMessage="Range (min:max)"
|
||||
/>
|
||||
),
|
||||
|
@ -134,7 +134,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
|||
field: 'text',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.color.textColorLabel"
|
||||
id="indexPatternFieldEditor.color.textColorLabel"
|
||||
defaultMessage="Text color"
|
||||
/>
|
||||
),
|
||||
|
@ -158,7 +158,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
|||
field: 'background',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.color.backgroundLabel"
|
||||
id="indexPatternFieldEditor.color.backgroundLabel"
|
||||
defaultMessage="Background color"
|
||||
/>
|
||||
),
|
||||
|
@ -181,7 +181,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
|||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.color.exampleLabel"
|
||||
id="indexPatternFieldEditor.color.exampleLabel"
|
||||
defaultMessage="Example"
|
||||
/>
|
||||
),
|
||||
|
@ -200,15 +200,15 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
|||
},
|
||||
{
|
||||
field: 'actions',
|
||||
name: i18n.translate('indexPatternManagement.color.actions', {
|
||||
name: i18n.translate('indexPatternFieldEditor.color.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
name: i18n.translate('indexPatternManagement.color.deleteAria', {
|
||||
name: i18n.translate('indexPatternFieldEditor.color.deleteAria', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
description: i18n.translate('indexPatternManagement.color.deleteTitle', {
|
||||
description: i18n.translate('indexPatternFieldEditor.color.deleteTitle', {
|
||||
defaultMessage: 'Delete color format',
|
||||
}),
|
||||
onClick: (item: IndexedColor) => {
|
||||
|
@ -229,7 +229,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
|||
<EuiSpacer size="m" />
|
||||
<EuiButton iconType="plusInCircle" size="s" onClick={this.addColor}>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.color.addColorButton"
|
||||
id="indexPatternFieldEditor.color.addColorButton"
|
||||
defaultMessage="Add color"
|
||||
/>
|
||||
</EuiButton>
|
|
@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Documentation"
|
||||
id="indexPatternManagement.date.documentationLabel"
|
||||
id="indexPatternFieldEditor.date.documentationLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
||||
|
@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = `
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
||||
id="indexPatternManagement.date.momentLabel"
|
||||
id="indexPatternFieldEditor.date.momentLabel"
|
||||
values={
|
||||
Object {
|
||||
"defaultPattern": <EuiCode>
|
|
@ -41,7 +41,7 @@ export class DateFormatEditor extends DefaultFormatEditor<DateFormatEditorFormat
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.date.momentLabel"
|
||||
id="indexPatternFieldEditor.date.momentLabel"
|
||||
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
||||
values={{
|
||||
defaultPattern: <EuiCode>{defaultPattern}</EuiCode>,
|
||||
|
@ -54,7 +54,7 @@ export class DateFormatEditor extends DefaultFormatEditor<DateFormatEditorFormat
|
|||
<span>
|
||||
<EuiLink target="_blank" href="https://momentjs.com/">
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.date.documentationLabel"
|
||||
id="indexPatternFieldEditor.date.documentationLabel"
|
||||
defaultMessage="Documentation"
|
||||
/>
|
||||
|
|
@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Documentation"
|
||||
id="indexPatternManagement.date.documentationLabel"
|
||||
id="indexPatternFieldEditor.date.documentationLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
||||
|
@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = `
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
||||
id="indexPatternManagement.date.momentLabel"
|
||||
id="indexPatternFieldEditor.date.momentLabel"
|
||||
values={
|
||||
Object {
|
||||
"defaultPattern": <EuiCode>
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FieldFormat } from '../../../../../../../../data/public';
|
||||
import type { FieldFormat } from 'src/plugins/data/public';
|
||||
|
||||
import { DateNanosFormatEditor } from './date_nanos';
|
||||
|
|
@ -40,7 +40,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor<DateNanosFormatEd
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.date.momentLabel"
|
||||
id="indexPatternFieldEditor.date.momentLabel"
|
||||
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
||||
values={{
|
||||
defaultPattern: <EuiCode>{defaultPattern}</EuiCode>,
|
||||
|
@ -53,7 +53,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor<DateNanosFormatEd
|
|||
<span>
|
||||
<EuiLink target="_blank" href="https://momentjs.com/">
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.date.documentationLabel"
|
||||
id="indexPatternFieldEditor.date.documentationLabel"
|
||||
defaultMessage="Documentation"
|
||||
/>
|
||||
|
|
@ -10,8 +10,8 @@ import React, { PureComponent, ReactText } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FieldFormat, FieldFormatsContentType } from 'src/plugins/data/public';
|
||||
import { Sample } from '../../../../types';
|
||||
import { FieldFormatEditorProps } from '../../field_format_editor';
|
||||
import { Sample } from '../../types';
|
||||
import { FormatSelectEditorProps } from '../../field_format_editor';
|
||||
|
||||
export type ConverterParams = string | number | Array<string | number>;
|
||||
|
||||
|
@ -30,7 +30,7 @@ export const convertSampleInput = (
|
|||
};
|
||||
});
|
||||
} catch (e) {
|
||||
error = i18n.translate('indexPatternManagement.defaultErrorMessage', {
|
||||
error = i18n.translate('indexPatternFieldEditor.defaultErrorMessage', {
|
||||
defaultMessage: 'An error occurred while trying to use this format configuration: {message}',
|
||||
values: { message: e.message },
|
||||
});
|
||||
|
@ -51,7 +51,7 @@ export interface FormatEditorProps<P> {
|
|||
format: FieldFormat;
|
||||
formatParams: { type?: string } & P;
|
||||
onChange: (newParams: Record<string, any>) => void;
|
||||
onError: FieldFormatEditorProps['onError'];
|
||||
onError: FormatSelectEditorProps['onError'];
|
||||
}
|
||||
|
||||
export interface FormatEditorState {
|
|
@ -12,7 +12,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] =
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Input format"
|
||||
id="indexPatternManagement.duration.inputFormatLabel"
|
||||
id="indexPatternFieldEditor.duration.inputFormatLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] =
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Output format"
|
||||
id="indexPatternManagement.duration.outputFormatLabel"
|
||||
id="indexPatternFieldEditor.duration.outputFormatLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Input format"
|
||||
id="indexPatternManagement.duration.inputFormatLabel"
|
||||
id="indexPatternFieldEditor.duration.inputFormatLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Output format"
|
||||
id="indexPatternManagement.duration.outputFormatLabel"
|
||||
id="indexPatternFieldEditor.duration.outputFormatLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Decimal places"
|
||||
id="indexPatternManagement.duration.decimalPlacesLabel"
|
||||
id="indexPatternFieldEditor.duration.decimalPlacesLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Show suffix"
|
||||
id="indexPatternManagement.duration.showSuffixLabel"
|
||||
id="indexPatternFieldEditor.duration.showSuffixLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
|
@ -65,7 +65,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
|||
!(nextProps.format as DurationFormat).isHuman() &&
|
||||
nextProps.formatParams.outputPrecision > 20
|
||||
) {
|
||||
error = i18n.translate('indexPatternManagement.durationErrorMessage', {
|
||||
error = i18n.translate('indexPatternFieldEditor.durationErrorMessage', {
|
||||
defaultMessage: 'Decimal places must be between 0 and 20',
|
||||
});
|
||||
nextProps.onError(error);
|
||||
|
@ -91,7 +91,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.duration.inputFormatLabel"
|
||||
id="indexPatternFieldEditor.duration.inputFormatLabel"
|
||||
defaultMessage="Input format"
|
||||
/>
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.duration.outputFormatLabel"
|
||||
id="indexPatternFieldEditor.duration.outputFormatLabel"
|
||||
defaultMessage="Output format"
|
||||
/>
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.duration.decimalPlacesLabel"
|
||||
id="indexPatternFieldEditor.duration.decimalPlacesLabel"
|
||||
defaultMessage="Decimal places"
|
||||
/>
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
|||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.duration.showSuffixLabel"
|
||||
id="indexPatternFieldEditor.duration.showSuffixLabel"
|
||||
defaultMessage="Show suffix"
|
||||
/>
|
||||
}
|
|
@ -16,7 +16,7 @@ exports[`NumberFormatEditor should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Documentation"
|
||||
id="indexPatternManagement.number.documentationLabel"
|
||||
id="indexPatternFieldEditor.number.documentationLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
||||
|
@ -30,7 +30,7 @@ exports[`NumberFormatEditor should render normally 1`] = `
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
||||
id="indexPatternManagement.number.numeralLabel"
|
||||
id="indexPatternFieldEditor.number.numeralLabel"
|
||||
values={
|
||||
Object {
|
||||
"defaultPattern": <EuiCode>
|
|
@ -36,7 +36,7 @@ export class NumberFormatEditor extends DefaultFormatEditor<NumberFormatEditorPa
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.number.numeralLabel"
|
||||
id="indexPatternFieldEditor.number.numeralLabel"
|
||||
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
||||
values={{ defaultPattern: <EuiCode>{defaultPattern}</EuiCode> }}
|
||||
/>
|
||||
|
@ -45,7 +45,7 @@ export class NumberFormatEditor extends DefaultFormatEditor<NumberFormatEditorPa
|
|||
<span>
|
||||
<EuiLink target="_blank" href="https://adamwdraper.github.io/Numeral-js/">
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.number.documentationLabel"
|
||||
id="indexPatternFieldEditor.number.documentationLabel"
|
||||
defaultMessage="Documentation"
|
||||
/>
|
||||
|
|
@ -16,7 +16,7 @@ exports[`PercentFormatEditor should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Documentation"
|
||||
id="indexPatternManagement.number.documentationLabel"
|
||||
id="indexPatternFieldEditor.number.documentationLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
||||
|
@ -30,7 +30,7 @@ exports[`PercentFormatEditor should render normally 1`] = `
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
||||
id="indexPatternManagement.number.numeralLabel"
|
||||
id="indexPatternFieldEditor.number.numeralLabel"
|
||||
values={
|
||||
Object {
|
||||
"defaultPattern": <EuiCode>
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FieldFormat } from '../../../../../../../../data/public';
|
||||
import { FieldFormat } from 'src/plugins/data/public';
|
||||
|
||||
import { PercentFormatEditor } from './percent';
|
||||
|
|
@ -9,7 +9,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn
|
|||
"field": "key",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Key"
|
||||
id="indexPatternManagement.staticLookup.keyLabel"
|
||||
id="indexPatternFieldEditor.staticLookup.keyLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -18,7 +18,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn
|
|||
"field": "value",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Value"
|
||||
id="indexPatternManagement.staticLookup.valueLabel"
|
||||
id="indexPatternFieldEditor.staticLookup.valueLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -73,7 +73,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add entry"
|
||||
id="indexPatternManagement.staticLookup.addEntryButton"
|
||||
id="indexPatternFieldEditor.staticLookup.addEntryButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
|
@ -89,7 +89,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Value for unknown key"
|
||||
id="indexPatternManagement.staticLookup.unknownKeyLabel"
|
||||
id="indexPatternFieldEditor.staticLookup.unknownKeyLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = `
|
|||
"field": "key",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Key"
|
||||
id="indexPatternManagement.staticLookup.keyLabel"
|
||||
id="indexPatternFieldEditor.staticLookup.keyLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -125,7 +125,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = `
|
|||
"field": "value",
|
||||
"name": <FormattedMessage
|
||||
defaultMessage="Value"
|
||||
id="indexPatternManagement.staticLookup.valueLabel"
|
||||
id="indexPatternFieldEditor.staticLookup.valueLabel"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"render": [Function],
|
||||
|
@ -174,7 +174,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = `
|
|||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add entry"
|
||||
id="indexPatternManagement.staticLookup.addEntryButton"
|
||||
id="indexPatternFieldEditor.staticLookup.addEntryButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
|
@ -190,7 +190,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = `
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Value for unknown key"
|
||||
id="indexPatternManagement.staticLookup.unknownKeyLabel"
|
||||
id="indexPatternFieldEditor.staticLookup.unknownKeyLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
import React from 'react';
|
||||
import { shallowWithI18nProvider } from '@kbn/test/jest';
|
||||
import { StaticLookupFormatEditorFormatParams } from './static_lookup';
|
||||
import { FieldFormat } from '../../../../../../../../data/public';
|
||||
import { FieldFormat } from 'src/plugins/data/public';
|
||||
|
||||
import { StaticLookupFormatEditor } from './static_lookup';
|
||||
|
|
@ -72,7 +72,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
|||
field: 'key',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.staticLookup.keyLabel"
|
||||
id="indexPatternFieldEditor.staticLookup.keyLabel"
|
||||
defaultMessage="Key"
|
||||
/>
|
||||
),
|
||||
|
@ -96,7 +96,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
|||
field: 'value',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.staticLookup.valueLabel"
|
||||
id="indexPatternFieldEditor.staticLookup.valueLabel"
|
||||
defaultMessage="Value"
|
||||
/>
|
||||
),
|
||||
|
@ -118,15 +118,15 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
|||
},
|
||||
{
|
||||
field: 'actions',
|
||||
name: i18n.translate('indexPatternManagement.staticLookup.actions', {
|
||||
name: i18n.translate('indexPatternFieldEditor.staticLookup.actions', {
|
||||
defaultMessage: 'actions',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
name: i18n.translate('indexPatternManagement.staticLookup.deleteAria', {
|
||||
name: i18n.translate('indexPatternFieldEditor.staticLookup.deleteAria', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
description: i18n.translate('indexPatternManagement.staticLookup.deleteTitle', {
|
||||
description: i18n.translate('indexPatternFieldEditor.staticLookup.deleteTitle', {
|
||||
defaultMessage: 'Delete entry',
|
||||
}),
|
||||
onClick: (item: StaticLookupItem) => {
|
||||
|
@ -148,7 +148,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
|||
<EuiSpacer size="m" />
|
||||
<EuiButton iconType="plusInCircle" size="s" onClick={this.addLookup}>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.staticLookup.addEntryButton"
|
||||
id="indexPatternFieldEditor.staticLookup.addEntryButton"
|
||||
defaultMessage="Add entry"
|
||||
/>
|
||||
</EuiButton>
|
||||
|
@ -156,7 +156,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.staticLookup.unknownKeyLabel"
|
||||
id="indexPatternFieldEditor.staticLookup.unknownKeyLabel"
|
||||
defaultMessage="Value for unknown key"
|
||||
/>
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
|||
<EuiFieldText
|
||||
value={formatParams.unknownKeyValue || ''}
|
||||
placeholder={i18n.translate(
|
||||
'indexPatternManagement.staticLookup.leaveBlankPlaceholder',
|
||||
'indexPatternFieldEditor.staticLookup.leaveBlankPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Leave blank to keep value as-is',
|
||||
}
|
|
@ -12,7 +12,7 @@ exports[`StringFormatEditor should render normally 1`] = `
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Transform"
|
||||
id="indexPatternManagement.string.transformLabel"
|
||||
id="indexPatternFieldEditor.string.transformLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
|
@ -47,7 +47,7 @@ export class StringFormatEditor extends DefaultFormatEditor<StringFormatEditorFo
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.string.transformLabel"
|
||||
id="indexPatternFieldEditor.string.transformLabel"
|
||||
defaultMessage="Transform"
|
||||
/>
|
||||
}
|
|
@ -12,7 +12,7 @@ exports[`TruncateFormatEditor should render normally 1`] = `
|
|||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Field length"
|
||||
id="indexPatternManagement.truncate.lengthLabel"
|
||||
id="indexPatternFieldEditor.truncate.lengthLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
|
@ -37,7 +37,7 @@ export class TruncateFormatEditor extends DefaultFormatEditor<TruncateFormatEdit
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.truncate.lengthLabel"
|
||||
id="indexPatternFieldEditor.truncate.lengthLabel"
|
||||
defaultMessage="Field length"
|
||||
/>
|
||||
}
|
|
@ -166,14 +166,26 @@ exports[`UrlFormatEditor should render normally 1`] = `
|
|||
class="euiFormHelpText euiFormRow__text"
|
||||
id="generated-id-help"
|
||||
>
|
||||
<button
|
||||
<a
|
||||
class="euiLink euiLink--primary"
|
||||
type="button"
|
||||
href="https://www.elastic.co/guide/en/kibana/mocked-test-branch/field-formatters-string.html"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<span>
|
||||
URL template help
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
aria-label="External link"
|
||||
class="euiLink__externalIcon"
|
||||
data-euiicon-type="popout"
|
||||
/>
|
||||
<span
|
||||
class="euiScreenReaderOnly"
|
||||
>
|
||||
(opens in a new tab or window)
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -217,14 +229,26 @@ exports[`UrlFormatEditor should render normally 1`] = `
|
|||
class="euiFormHelpText euiFormRow__text"
|
||||
id="generated-id-help"
|
||||
>
|
||||
<button
|
||||
<a
|
||||
class="euiLink euiLink--primary"
|
||||
type="button"
|
||||
href="https://www.elastic.co/guide/en/kibana/mocked-test-branch/field-formatters-string.html"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<span>
|
||||
Label template help
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
aria-label="External link"
|
||||
class="euiLink__externalIcon"
|
||||
data-euiicon-type="popout"
|
||||
/>
|
||||
<span
|
||||
class="euiScreenReaderOnly"
|
||||
>
|
||||
(opens in a new tab or window)
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -10,8 +10,8 @@ import React from 'react';
|
|||
import { FieldFormat } from 'src/plugins/data/public';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { UrlFormatEditor } from './url';
|
||||
import { coreMock } from '../../../../../../../../../core/public/mocks';
|
||||
import { createKibanaReactContext } from '../../../../../../../../kibana_react/public';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { createKibanaReactContext } from '../../../../../../kibana_react/public';
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
|
@ -76,38 +76,6 @@ describe('UrlFormatEditor', () => {
|
|||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render url template help', async () => {
|
||||
const { getByText, getByTestId } = renderWithContext(
|
||||
<UrlFormatEditor
|
||||
fieldType={fieldType}
|
||||
format={format}
|
||||
formatParams={formatParams}
|
||||
onChange={onChange}
|
||||
onError={onError}
|
||||
/>
|
||||
);
|
||||
|
||||
getByText('URL template help');
|
||||
userEvent.click(getByText('URL template help'));
|
||||
expect(getByTestId('urlTemplateFlyoutTestSubj')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render label template help', async () => {
|
||||
const { getByText, getByTestId } = renderWithContext(
|
||||
<UrlFormatEditor
|
||||
fieldType={fieldType}
|
||||
format={format}
|
||||
formatParams={formatParams}
|
||||
onChange={onChange}
|
||||
onError={onError}
|
||||
/>
|
||||
);
|
||||
|
||||
getByText('Label template help');
|
||||
userEvent.click(getByText('Label template help'));
|
||||
expect(getByTestId('labelTemplateFlyoutTestSubj')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render width and height fields if image', async () => {
|
||||
const { getByLabelText } = renderWithContext(
|
||||
<UrlFormatEditor
|
|
@ -22,11 +22,7 @@ import { DefaultFormatEditor, FormatEditorProps } from '../default';
|
|||
|
||||
import { FormatEditorSamples } from '../../samples';
|
||||
|
||||
import { LabelTemplateFlyout } from './label_template_flyout';
|
||||
|
||||
import { UrlTemplateFlyout } from './url_template_flyout';
|
||||
import type { IndexPatternManagmentContextValue } from '../../../../../../types';
|
||||
import { context as contextType } from '../../../../../../../../kibana_react/public';
|
||||
import { context as contextType } from '../../../../../../kibana_react/public';
|
||||
|
||||
interface OnChangeParam {
|
||||
type: string;
|
||||
|
@ -59,9 +55,6 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
|||
> {
|
||||
static contextType = contextType;
|
||||
static formatId = 'url';
|
||||
// TODO: @kbn/optimizer can't compile this
|
||||
// declare context: IndexPatternManagmentContextValue;
|
||||
context: IndexPatternManagmentContextValue | undefined;
|
||||
private get sampleIconPath() {
|
||||
const sampleIconPath = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`;
|
||||
return this.context?.services.http
|
||||
|
@ -110,32 +103,6 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
|||
this.onChange(params);
|
||||
};
|
||||
|
||||
showUrlTemplateHelp = () => {
|
||||
this.setState({
|
||||
showLabelTemplateHelp: false,
|
||||
showUrlTemplateHelp: true,
|
||||
});
|
||||
};
|
||||
|
||||
hideUrlTemplateHelp = () => {
|
||||
this.setState({
|
||||
showUrlTemplateHelp: false,
|
||||
});
|
||||
};
|
||||
|
||||
showLabelTemplateHelp = () => {
|
||||
this.setState({
|
||||
showLabelTemplateHelp: true,
|
||||
showUrlTemplateHelp: false,
|
||||
});
|
||||
};
|
||||
|
||||
hideLabelTemplateHelp = () => {
|
||||
this.setState({
|
||||
showLabelTemplateHelp: false,
|
||||
});
|
||||
};
|
||||
|
||||
renderWidthHeightParameters = () => {
|
||||
const width = this.sanitizeNumericValue(this.props.formatParams.width);
|
||||
const height = this.sanitizeNumericValue(this.props.formatParams.height);
|
||||
|
@ -143,7 +110,7 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
|||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage id="indexPatternManagement.url.widthLabel" defaultMessage="Width" />
|
||||
<FormattedMessage id="indexPatternFieldEditor.url.widthLabel" defaultMessage="Width" />
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
|
@ -156,7 +123,10 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
|||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage id="indexPatternManagement.url.heightLabel" defaultMessage="Height" />
|
||||
<FormattedMessage
|
||||
id="indexPatternFieldEditor.url.heightLabel"
|
||||
defaultMessage="Height"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
|
@ -177,17 +147,9 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
|||
|
||||
return (
|
||||
<Fragment>
|
||||
<LabelTemplateFlyout
|
||||
isVisible={this.state.showLabelTemplateHelp}
|
||||
onClose={this.hideLabelTemplateHelp}
|
||||
/>
|
||||
<UrlTemplateFlyout
|
||||
isVisible={this.state.showUrlTemplateHelp}
|
||||
onClose={this.hideUrlTemplateHelp}
|
||||
/>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage id="indexPatternManagement.url.typeLabel" defaultMessage="Type" />
|
||||
<FormattedMessage id="indexPatternFieldEditor.url.typeLabel" defaultMessage="Type" />
|
||||
}
|
||||
>
|
||||
<EuiSelect
|
||||
|
@ -209,7 +171,7 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.url.openTabLabel"
|
||||
id="indexPatternFieldEditor.url.openTabLabel"
|
||||
defaultMessage="Open in a new tab"
|
||||
/>
|
||||
}
|
||||
|
@ -217,9 +179,12 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
|||
<EuiSwitch
|
||||
label={
|
||||
formatParams.openLinkInCurrentTab ? (
|
||||
<FormattedMessage id="indexPatternManagement.url.offLabel" defaultMessage="Off" />
|
||||
<FormattedMessage
|
||||
id="indexPatternFieldEditor.url.offLabel"
|
||||
defaultMessage="Off"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage id="indexPatternManagement.url.onLabel" defaultMessage="On" />
|
||||
<FormattedMessage id="indexPatternFieldEditor.url.onLabel" defaultMessage="On" />
|
||||
)
|
||||
}
|
||||
checked={!formatParams.openLinkInCurrentTab}
|
||||
|
@ -233,14 +198,17 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.url.urlTemplateLabel"
|
||||
id="indexPatternFieldEditor.url.urlTemplateLabel"
|
||||
defaultMessage="URL template"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<EuiLink onClick={this.showUrlTemplateHelp}>
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href={this.context.services.docLinks.links.indexPatterns.fieldFormattersString}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.url.template.helpLinkText"
|
||||
id="indexPatternFieldEditor.url.template.helpLinkText"
|
||||
defaultMessage="URL template help"
|
||||
/>
|
||||
</EuiLink>
|
||||
|
@ -260,14 +228,17 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
|||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.url.labelTemplateLabel"
|
||||
id="indexPatternFieldEditor.url.labelTemplateLabel"
|
||||
defaultMessage="Label template"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<EuiLink onClick={this.showLabelTemplateHelp}>
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href={this.context.services.docLinks.links.indexPatterns.fieldFormattersString}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.url.labelTemplateHelpText"
|
||||
id="indexPatternFieldEditor.url.labelTemplateHelpText"
|
||||
defaultMessage="Label template help"
|
||||
/>
|
||||
</EuiLink>
|