|
@ -29,6 +29,7 @@
|
||||||
"maps_legacy": "src/plugins/maps_legacy",
|
"maps_legacy": "src/plugins/maps_legacy",
|
||||||
"monaco": "packages/kbn-monaco/src",
|
"monaco": "packages/kbn-monaco/src",
|
||||||
"presentationUtil": "src/plugins/presentation_util",
|
"presentationUtil": "src/plugins/presentation_util",
|
||||||
|
"indexPatternFieldEditor": "src/plugins/index_pattern_field_editor",
|
||||||
"indexPatternManagement": "src/plugins/index_pattern_management",
|
"indexPatternManagement": "src/plugins/index_pattern_management",
|
||||||
"advancedSettings": "src/plugins/advanced_settings",
|
"advancedSettings": "src/plugins/advanced_settings",
|
||||||
"kibana_legacy": "src/plugins/kibana_legacy",
|
"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.
|
|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]
|
|{kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement]
|
||||||
|WARNING: Missing README.
|
|WARNING: Missing README.
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,12 @@ const toDiagnostics = (error: PainlessError): monaco.editor.IMarkerData => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface SyntaxErrors {
|
||||||
|
[modelId: string]: PainlessError[];
|
||||||
|
}
|
||||||
export class DiagnosticsAdapter {
|
export class DiagnosticsAdapter {
|
||||||
|
private errors: SyntaxErrors = {};
|
||||||
|
|
||||||
constructor(private worker: WorkerAccessor) {
|
constructor(private worker: WorkerAccessor) {
|
||||||
const onModelAdd = (model: monaco.editor.IModel): void => {
|
const onModelAdd = (model: monaco.editor.IModel): void => {
|
||||||
let handle: any;
|
let handle: any;
|
||||||
|
@ -55,8 +60,16 @@ export class DiagnosticsAdapter {
|
||||||
|
|
||||||
if (errorMarkers) {
|
if (errorMarkers) {
|
||||||
const model = monaco.editor.getModel(resource);
|
const model = monaco.editor.getModel(resource);
|
||||||
|
this.errors = {
|
||||||
|
...this.errors,
|
||||||
|
[model!.id]: errorMarkers,
|
||||||
|
};
|
||||||
// Set the error markers and underline them with "Error" severity
|
// Set the error markers and underline them with "Error" severity
|
||||||
monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics));
|
monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSyntaxErrors() {
|
||||||
|
return this.errors;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,14 @@
|
||||||
|
|
||||||
import { ID } from './constants';
|
import { ID } from './constants';
|
||||||
import { lexerRules, languageConfiguration } from './lexer_rules';
|
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';
|
export { PainlessContext, PainlessAutocompleteField } from './types';
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { ID } from './constants';
|
||||||
import { PainlessContext, PainlessAutocompleteField } from './types';
|
import { PainlessContext, PainlessAutocompleteField } from './types';
|
||||||
import { PainlessWorker } from './worker';
|
import { PainlessWorker } from './worker';
|
||||||
import { PainlessCompletionAdapter } from './completion_adapter';
|
import { PainlessCompletionAdapter } from './completion_adapter';
|
||||||
import { DiagnosticsAdapter } from './diagnostics_adapter';
|
import { DiagnosticsAdapter, SyntaxErrors } from './diagnostics_adapter';
|
||||||
|
|
||||||
const workerProxyService = new WorkerProxyService();
|
const workerProxyService = new WorkerProxyService();
|
||||||
const editorStateService = new EditorStateService();
|
const editorStateService = new EditorStateService();
|
||||||
|
@ -33,8 +33,15 @@ export const getSuggestionProvider = (
|
||||||
return new PainlessCompletionAdapter(worker, editorStateService);
|
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 () => {
|
monaco.languages.onLanguage(ID, async () => {
|
||||||
workerProxyService.setup();
|
workerProxyService.setup();
|
||||||
|
|
||||||
new DiagnosticsAdapter(worker);
|
diagnosticsAdapter = new DiagnosticsAdapter(worker);
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,7 +33,7 @@ pageLoadAssetSize:
|
||||||
home: 41661
|
home: 41661
|
||||||
indexLifecycleManagement: 107090
|
indexLifecycleManagement: 107090
|
||||||
indexManagement: 140608
|
indexManagement: 140608
|
||||||
indexPatternManagement: 154222
|
indexPatternManagement: 28222
|
||||||
infra: 204800
|
infra: 204800
|
||||||
fleet: 415829
|
fleet: 415829
|
||||||
ingestPipelines: 58003
|
ingestPipelines: 58003
|
||||||
|
@ -103,6 +103,7 @@ pageLoadAssetSize:
|
||||||
stackAlerts: 29684
|
stackAlerts: 29684
|
||||||
presentationUtil: 28545
|
presentationUtil: 28545
|
||||||
spacesOss: 18817
|
spacesOss: 18817
|
||||||
|
indexPatternFieldEditor: 90489
|
||||||
osquery: 107090
|
osquery: 107090
|
||||||
fileUpload: 25664
|
fileUpload: 25664
|
||||||
banners: 17946
|
banners: 17946
|
||||||
|
|
|
@ -121,6 +121,7 @@ export class DocLinksService {
|
||||||
indexPatterns: {
|
indexPatterns: {
|
||||||
loadingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/tutorial-load-dataset.html`,
|
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`,
|
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`,
|
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`,
|
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,
|
"count": 1,
|
||||||
"esTypes": Array [
|
"esTypes": Array [
|
||||||
"text",
|
"keyword",
|
||||||
],
|
],
|
||||||
"lang": "lang",
|
"lang": "lang",
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
@ -49,7 +49,7 @@ Object {
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"customLabel": undefined,
|
"customLabel": undefined,
|
||||||
"esTypes": Array [
|
"esTypes": Array [
|
||||||
"text",
|
"keyword",
|
||||||
],
|
],
|
||||||
"format": Object {
|
"format": Object {
|
||||||
"id": "number",
|
"id": "number",
|
||||||
|
|
|
@ -26,7 +26,7 @@ describe('Field', function () {
|
||||||
script: 'script',
|
script: 'script',
|
||||||
lang: 'lang',
|
lang: 'lang',
|
||||||
count: 1,
|
count: 1,
|
||||||
esTypes: ['text'],
|
esTypes: ['text'], // note, this will get replaced by the runtime field type
|
||||||
aggregatable: true,
|
aggregatable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
|
@ -71,7 +71,7 @@ describe('Field', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets type field when _source field', () => {
|
it('sets type field when _source field', () => {
|
||||||
const field = getField({ name: '_source' });
|
const field = getField({ name: '_source', runtimeField: undefined });
|
||||||
expect(field.type).toEqual('_source');
|
expect(field.type).toEqual('_source');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { RuntimeField } from '../types';
|
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 { KBN_FIELD_TYPES } from '../../kbn_field_types/types';
|
||||||
import type { IFieldType } from './types';
|
import type { IFieldType } from './types';
|
||||||
import { FieldSpec, IndexPattern } from '../..';
|
import { FieldSpec, IndexPattern } from '../..';
|
||||||
|
@ -99,11 +99,13 @@ export class IndexPatternField implements IFieldType {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get type() {
|
public get type() {
|
||||||
return this.spec.type;
|
return this.runtimeField?.type
|
||||||
|
? castEsToKbnFieldTypeName(this.runtimeField?.type)
|
||||||
|
: this.spec.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get esTypes() {
|
public get esTypes() {
|
||||||
return this.spec.esTypes;
|
return this.runtimeField?.type ? [this.runtimeField?.type] : this.spec.esTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get scripted() {
|
public get scripted() {
|
||||||
|
|
|
@ -12,3 +12,4 @@ export { IndexPatternsService, IndexPatternsContract } from './index_patterns';
|
||||||
export type { IndexPattern } from './index_patterns';
|
export type { IndexPattern } from './index_patterns';
|
||||||
export * from './errors';
|
export * from './errors';
|
||||||
export * from './expressions';
|
export * from './expressions';
|
||||||
|
export * from './constants';
|
||||||
|
|
|
@ -565,7 +565,9 @@ Object {
|
||||||
"conflictDescriptions": undefined,
|
"conflictDescriptions": undefined,
|
||||||
"count": 0,
|
"count": 0,
|
||||||
"customLabel": undefined,
|
"customLabel": undefined,
|
||||||
"esTypes": undefined,
|
"esTypes": Array [
|
||||||
|
"keyword",
|
||||||
|
],
|
||||||
"format": Object {
|
"format": Object {
|
||||||
"id": "number",
|
"id": "number",
|
||||||
"params": Object {
|
"params": Object {
|
||||||
|
@ -587,7 +589,7 @@ Object {
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"shortDotsEnable": false,
|
"shortDotsEnable": false,
|
||||||
"subType": undefined,
|
"subType": undefined,
|
||||||
"type": undefined,
|
"type": "string",
|
||||||
},
|
},
|
||||||
"script date": Object {
|
"script date": Object {
|
||||||
"aggregatable": true,
|
"aggregatable": true,
|
||||||
|
|
|
@ -389,6 +389,8 @@ export class IndexPattern implements IIndexPattern {
|
||||||
existingField.runtimeField = undefined;
|
existingField.runtimeField = undefined;
|
||||||
} else {
|
} else {
|
||||||
// runtimeField only
|
// runtimeField only
|
||||||
|
this.setFieldCustomLabel(name, null);
|
||||||
|
this.deleteFieldFormat(name);
|
||||||
this.fields.remove(existingField);
|
this.fields.remove(existingField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -423,7 +425,6 @@ export class IndexPattern implements IIndexPattern {
|
||||||
|
|
||||||
if (fieldObject) {
|
if (fieldObject) {
|
||||||
fieldObject.customLabel = newCustomLabel;
|
fieldObject.customLabel = newCustomLabel;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setFieldAttrs(fieldName, 'customLabel', newCustomLabel);
|
this.setFieldAttrs(fieldName, 'customLabel', newCustomLabel);
|
||||||
|
|
|
@ -415,11 +415,10 @@ export class IndexPatternsService {
|
||||||
},
|
},
|
||||||
spec.fieldAttrs
|
spec.fieldAttrs
|
||||||
);
|
);
|
||||||
// APPLY RUNTIME FIELDS
|
// CREATE RUNTIME FIELDS
|
||||||
for (const [key, value] of Object.entries(runtimeFieldMap || {})) {
|
for (const [key, value] of Object.entries(runtimeFieldMap || {})) {
|
||||||
if (spec.fields[key]) {
|
// do not create runtime field if mapped field exists
|
||||||
spec.fields[key].runtimeField = value;
|
if (!spec.fields[key]) {
|
||||||
} else {
|
|
||||||
spec.fields[key] = {
|
spec.fields[key] = {
|
||||||
name: key,
|
name: key,
|
||||||
type: castEsToKbnFieldTypeName(value.type),
|
type: castEsToKbnFieldTypeName(value.type),
|
||||||
|
|
|
@ -10,15 +10,16 @@ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notificatio
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import type { SavedObject } from 'src/core/server';
|
import type { SavedObject } from 'src/core/server';
|
||||||
import { IFieldType } from './fields';
|
import { IFieldType } from './fields';
|
||||||
|
import { RUNTIME_FIELD_TYPES } from './constants';
|
||||||
import { SerializedFieldFormat } from '../../../expressions/common';
|
import { SerializedFieldFormat } from '../../../expressions/common';
|
||||||
import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..';
|
import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..';
|
||||||
|
|
||||||
export type FieldFormatMap = Record<string, SerializedFieldFormat>;
|
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 {
|
export interface RuntimeField {
|
||||||
type: RuntimeType;
|
type: RuntimeType;
|
||||||
script: {
|
script?: {
|
||||||
source: string;
|
source: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,11 +211,12 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
const addField: FormHook<T, I>['__addField'] = useCallback(
|
const addField: FormHook<T, I>['__addField'] = useCallback(
|
||||||
(field) => {
|
(field) => {
|
||||||
|
const fieldExists = fieldsRefs.current[field.path] !== undefined;
|
||||||
fieldsRefs.current[field.path] = field;
|
fieldsRefs.current[field.path] = field;
|
||||||
|
|
||||||
updateFormDataAt(field.path, field.value);
|
updateFormDataAt(field.path, field.value);
|
||||||
|
|
||||||
if (!field.isValidated) {
|
if (!fieldExists && !field.isValidated) {
|
||||||
setIsValid(undefined);
|
setIsValid(undefined);
|
||||||
|
|
||||||
// When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**.
|
// 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
|
<FormattedMessage
|
||||||
defaultMessage="Documentation"
|
defaultMessage="Documentation"
|
||||||
id="indexPatternManagement.number.documentationLabel"
|
id="indexPatternFieldEditor.number.documentationLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ exports[`BytesFormatEditor should render normally 1`] = `
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
||||||
id="indexPatternManagement.number.numeralLabel"
|
id="indexPatternFieldEditor.number.numeralLabel"
|
||||||
values={
|
values={
|
||||||
Object {
|
Object {
|
||||||
"defaultPattern": <EuiCode>
|
"defaultPattern": <EuiCode>
|
|
@ -9,7 +9,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
||||||
"field": "regex",
|
"field": "regex",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Pattern (regular expression)"
|
defaultMessage="Pattern (regular expression)"
|
||||||
id="indexPatternManagement.color.patternLabel"
|
id="indexPatternFieldEditor.color.patternLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -18,7 +18,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
||||||
"field": "text",
|
"field": "text",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Text color"
|
defaultMessage="Text color"
|
||||||
id="indexPatternManagement.color.textColorLabel"
|
id="indexPatternFieldEditor.color.textColorLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -27,7 +27,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
||||||
"field": "background",
|
"field": "background",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Background color"
|
defaultMessage="Background color"
|
||||||
id="indexPatternManagement.color.backgroundLabel"
|
id="indexPatternFieldEditor.color.backgroundLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -35,7 +35,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Example"
|
defaultMessage="Example"
|
||||||
id="indexPatternManagement.color.exampleLabel"
|
id="indexPatternFieldEditor.color.exampleLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -89,7 +89,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = `
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Add color"
|
defaultMessage="Add color"
|
||||||
id="indexPatternManagement.color.addColorButton"
|
id="indexPatternFieldEditor.color.addColorButton"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
|
@ -108,7 +108,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
||||||
"field": "range",
|
"field": "range",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Range (min:max)"
|
defaultMessage="Range (min:max)"
|
||||||
id="indexPatternManagement.color.rangeLabel"
|
id="indexPatternFieldEditor.color.rangeLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -117,7 +117,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
||||||
"field": "text",
|
"field": "text",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Text color"
|
defaultMessage="Text color"
|
||||||
id="indexPatternManagement.color.textColorLabel"
|
id="indexPatternFieldEditor.color.textColorLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -126,7 +126,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
||||||
"field": "background",
|
"field": "background",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Background color"
|
defaultMessage="Background color"
|
||||||
id="indexPatternManagement.color.backgroundLabel"
|
id="indexPatternFieldEditor.color.backgroundLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -134,7 +134,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
||||||
Object {
|
Object {
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Example"
|
defaultMessage="Example"
|
||||||
id="indexPatternManagement.color.exampleLabel"
|
id="indexPatternFieldEditor.color.exampleLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -181,7 +181,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] =
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Add color"
|
defaultMessage="Add color"
|
||||||
id="indexPatternManagement.color.addColorButton"
|
id="indexPatternFieldEditor.color.addColorButton"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
|
@ -200,7 +200,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
||||||
"field": "regex",
|
"field": "regex",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Pattern (regular expression)"
|
defaultMessage="Pattern (regular expression)"
|
||||||
id="indexPatternManagement.color.patternLabel"
|
id="indexPatternFieldEditor.color.patternLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -209,7 +209,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
||||||
"field": "text",
|
"field": "text",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Text color"
|
defaultMessage="Text color"
|
||||||
id="indexPatternManagement.color.textColorLabel"
|
id="indexPatternFieldEditor.color.textColorLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -218,7 +218,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
||||||
"field": "background",
|
"field": "background",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Background color"
|
defaultMessage="Background color"
|
||||||
id="indexPatternManagement.color.backgroundLabel"
|
id="indexPatternFieldEditor.color.backgroundLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -226,7 +226,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
||||||
Object {
|
Object {
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Example"
|
defaultMessage="Example"
|
||||||
id="indexPatternManagement.color.exampleLabel"
|
id="indexPatternFieldEditor.color.exampleLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -273,7 +273,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`]
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Add color"
|
defaultMessage="Add color"
|
||||||
id="indexPatternManagement.color.addColorButton"
|
id="indexPatternFieldEditor.color.addColorButton"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
</EuiButton>
|
</EuiButton>
|
|
@ -11,7 +11,7 @@ import { shallowWithI18nProvider } from '@kbn/test/jest';
|
||||||
import { FieldFormat } from 'src/plugins/data/public';
|
import { FieldFormat } from 'src/plugins/data/public';
|
||||||
|
|
||||||
import { ColorFormatEditor } from './color';
|
import { ColorFormatEditor } from './color';
|
||||||
import { fieldFormats } from '../../../../../../../../data/public';
|
import { fieldFormats } from '../../../../../../data/public';
|
||||||
|
|
||||||
const fieldType = 'string';
|
const fieldType = 'string';
|
||||||
const format = {
|
const format = {
|
|
@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { DefaultFormatEditor, FormatEditorProps } from '../default';
|
import { DefaultFormatEditor, FormatEditorProps } from '../default';
|
||||||
|
|
||||||
import { fieldFormats } from '../../../../../../../../../plugins/data/public';
|
import { fieldFormats } from '../../../../../../data/public';
|
||||||
|
|
||||||
interface Color {
|
interface Color {
|
||||||
range?: string;
|
range?: string;
|
||||||
|
@ -86,7 +86,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
||||||
field: 'regex',
|
field: 'regex',
|
||||||
name: (
|
name: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.color.patternLabel"
|
id="indexPatternFieldEditor.color.patternLabel"
|
||||||
defaultMessage="Pattern (regular expression)"
|
defaultMessage="Pattern (regular expression)"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -110,7 +110,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
||||||
field: 'range',
|
field: 'range',
|
||||||
name: (
|
name: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.color.rangeLabel"
|
id="indexPatternFieldEditor.color.rangeLabel"
|
||||||
defaultMessage="Range (min:max)"
|
defaultMessage="Range (min:max)"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -134,7 +134,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
||||||
field: 'text',
|
field: 'text',
|
||||||
name: (
|
name: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.color.textColorLabel"
|
id="indexPatternFieldEditor.color.textColorLabel"
|
||||||
defaultMessage="Text color"
|
defaultMessage="Text color"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -158,7 +158,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
||||||
field: 'background',
|
field: 'background',
|
||||||
name: (
|
name: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.color.backgroundLabel"
|
id="indexPatternFieldEditor.color.backgroundLabel"
|
||||||
defaultMessage="Background color"
|
defaultMessage="Background color"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -181,7 +181,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
||||||
{
|
{
|
||||||
name: (
|
name: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.color.exampleLabel"
|
id="indexPatternFieldEditor.color.exampleLabel"
|
||||||
defaultMessage="Example"
|
defaultMessage="Example"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -200,15 +200,15 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
name: i18n.translate('indexPatternManagement.color.actions', {
|
name: i18n.translate('indexPatternFieldEditor.color.actions', {
|
||||||
defaultMessage: 'Actions',
|
defaultMessage: 'Actions',
|
||||||
}),
|
}),
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
name: i18n.translate('indexPatternManagement.color.deleteAria', {
|
name: i18n.translate('indexPatternFieldEditor.color.deleteAria', {
|
||||||
defaultMessage: 'Delete',
|
defaultMessage: 'Delete',
|
||||||
}),
|
}),
|
||||||
description: i18n.translate('indexPatternManagement.color.deleteTitle', {
|
description: i18n.translate('indexPatternFieldEditor.color.deleteTitle', {
|
||||||
defaultMessage: 'Delete color format',
|
defaultMessage: 'Delete color format',
|
||||||
}),
|
}),
|
||||||
onClick: (item: IndexedColor) => {
|
onClick: (item: IndexedColor) => {
|
||||||
|
@ -229,7 +229,7 @@ export class ColorFormatEditor extends DefaultFormatEditor<ColorFormatEditorForm
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<EuiButton iconType="plusInCircle" size="s" onClick={this.addColor}>
|
<EuiButton iconType="plusInCircle" size="s" onClick={this.addColor}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.color.addColorButton"
|
id="indexPatternFieldEditor.color.addColorButton"
|
||||||
defaultMessage="Add color"
|
defaultMessage="Add color"
|
||||||
/>
|
/>
|
||||||
</EuiButton>
|
</EuiButton>
|
|
@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = `
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Documentation"
|
defaultMessage="Documentation"
|
||||||
id="indexPatternManagement.date.documentationLabel"
|
id="indexPatternFieldEditor.date.documentationLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = `
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
||||||
id="indexPatternManagement.date.momentLabel"
|
id="indexPatternFieldEditor.date.momentLabel"
|
||||||
values={
|
values={
|
||||||
Object {
|
Object {
|
||||||
"defaultPattern": <EuiCode>
|
"defaultPattern": <EuiCode>
|
|
@ -41,7 +41,7 @@ export class DateFormatEditor extends DefaultFormatEditor<DateFormatEditorFormat
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.date.momentLabel"
|
id="indexPatternFieldEditor.date.momentLabel"
|
||||||
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
||||||
values={{
|
values={{
|
||||||
defaultPattern: <EuiCode>{defaultPattern}</EuiCode>,
|
defaultPattern: <EuiCode>{defaultPattern}</EuiCode>,
|
||||||
|
@ -54,7 +54,7 @@ export class DateFormatEditor extends DefaultFormatEditor<DateFormatEditorFormat
|
||||||
<span>
|
<span>
|
||||||
<EuiLink target="_blank" href="https://momentjs.com/">
|
<EuiLink target="_blank" href="https://momentjs.com/">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.date.documentationLabel"
|
id="indexPatternFieldEditor.date.documentationLabel"
|
||||||
defaultMessage="Documentation"
|
defaultMessage="Documentation"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = `
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Documentation"
|
defaultMessage="Documentation"
|
||||||
id="indexPatternManagement.date.documentationLabel"
|
id="indexPatternFieldEditor.date.documentationLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = `
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
||||||
id="indexPatternManagement.date.momentLabel"
|
id="indexPatternFieldEditor.date.momentLabel"
|
||||||
values={
|
values={
|
||||||
Object {
|
Object {
|
||||||
"defaultPattern": <EuiCode>
|
"defaultPattern": <EuiCode>
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { FieldFormat } from '../../../../../../../../data/public';
|
import type { FieldFormat } from 'src/plugins/data/public';
|
||||||
|
|
||||||
import { DateNanosFormatEditor } from './date_nanos';
|
import { DateNanosFormatEditor } from './date_nanos';
|
||||||
|
|
|
@ -40,7 +40,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor<DateNanosFormatEd
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.date.momentLabel"
|
id="indexPatternFieldEditor.date.momentLabel"
|
||||||
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
defaultMessage="Moment.js format pattern (Default: {defaultPattern})"
|
||||||
values={{
|
values={{
|
||||||
defaultPattern: <EuiCode>{defaultPattern}</EuiCode>,
|
defaultPattern: <EuiCode>{defaultPattern}</EuiCode>,
|
||||||
|
@ -53,7 +53,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor<DateNanosFormatEd
|
||||||
<span>
|
<span>
|
||||||
<EuiLink target="_blank" href="https://momentjs.com/">
|
<EuiLink target="_blank" href="https://momentjs.com/">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.date.documentationLabel"
|
id="indexPatternFieldEditor.date.documentationLabel"
|
||||||
defaultMessage="Documentation"
|
defaultMessage="Documentation"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,8 +10,8 @@ import React, { PureComponent, ReactText } from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
import { FieldFormat, FieldFormatsContentType } from 'src/plugins/data/public';
|
import { FieldFormat, FieldFormatsContentType } from 'src/plugins/data/public';
|
||||||
import { Sample } from '../../../../types';
|
import { Sample } from '../../types';
|
||||||
import { FieldFormatEditorProps } from '../../field_format_editor';
|
import { FormatSelectEditorProps } from '../../field_format_editor';
|
||||||
|
|
||||||
export type ConverterParams = string | number | Array<string | number>;
|
export type ConverterParams = string | number | Array<string | number>;
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export const convertSampleInput = (
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = i18n.translate('indexPatternManagement.defaultErrorMessage', {
|
error = i18n.translate('indexPatternFieldEditor.defaultErrorMessage', {
|
||||||
defaultMessage: 'An error occurred while trying to use this format configuration: {message}',
|
defaultMessage: 'An error occurred while trying to use this format configuration: {message}',
|
||||||
values: { message: e.message },
|
values: { message: e.message },
|
||||||
});
|
});
|
||||||
|
@ -51,7 +51,7 @@ export interface FormatEditorProps<P> {
|
||||||
format: FieldFormat;
|
format: FieldFormat;
|
||||||
formatParams: { type?: string } & P;
|
formatParams: { type?: string } & P;
|
||||||
onChange: (newParams: Record<string, any>) => void;
|
onChange: (newParams: Record<string, any>) => void;
|
||||||
onError: FieldFormatEditorProps['onError'];
|
onError: FormatSelectEditorProps['onError'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormatEditorState {
|
export interface FormatEditorState {
|
|
@ -12,7 +12,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] =
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Input format"
|
defaultMessage="Input format"
|
||||||
id="indexPatternManagement.duration.inputFormatLabel"
|
id="indexPatternFieldEditor.duration.inputFormatLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] =
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Output format"
|
defaultMessage="Output format"
|
||||||
id="indexPatternManagement.duration.outputFormatLabel"
|
id="indexPatternFieldEditor.duration.outputFormatLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Input format"
|
defaultMessage="Input format"
|
||||||
id="indexPatternManagement.duration.inputFormatLabel"
|
id="indexPatternFieldEditor.duration.inputFormatLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Output format"
|
defaultMessage="Output format"
|
||||||
id="indexPatternManagement.duration.outputFormatLabel"
|
id="indexPatternFieldEditor.duration.outputFormatLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -189,7 +189,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Decimal places"
|
defaultMessage="Decimal places"
|
||||||
id="indexPatternManagement.duration.decimalPlacesLabel"
|
id="indexPatternFieldEditor.duration.decimalPlacesLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Show suffix"
|
defaultMessage="Show suffix"
|
||||||
id="indexPatternManagement.duration.showSuffixLabel"
|
id="indexPatternFieldEditor.duration.showSuffixLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
|
@ -65,7 +65,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
||||||
!(nextProps.format as DurationFormat).isHuman() &&
|
!(nextProps.format as DurationFormat).isHuman() &&
|
||||||
nextProps.formatParams.outputPrecision > 20
|
nextProps.formatParams.outputPrecision > 20
|
||||||
) {
|
) {
|
||||||
error = i18n.translate('indexPatternManagement.durationErrorMessage', {
|
error = i18n.translate('indexPatternFieldEditor.durationErrorMessage', {
|
||||||
defaultMessage: 'Decimal places must be between 0 and 20',
|
defaultMessage: 'Decimal places must be between 0 and 20',
|
||||||
});
|
});
|
||||||
nextProps.onError(error);
|
nextProps.onError(error);
|
||||||
|
@ -91,7 +91,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.duration.inputFormatLabel"
|
id="indexPatternFieldEditor.duration.inputFormatLabel"
|
||||||
defaultMessage="Input format"
|
defaultMessage="Input format"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.duration.outputFormatLabel"
|
id="indexPatternFieldEditor.duration.outputFormatLabel"
|
||||||
defaultMessage="Output format"
|
defaultMessage="Output format"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.duration.decimalPlacesLabel"
|
id="indexPatternFieldEditor.duration.decimalPlacesLabel"
|
||||||
defaultMessage="Decimal places"
|
defaultMessage="Decimal places"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ export class DurationFormatEditor extends DefaultFormatEditor<
|
||||||
<EuiSwitch
|
<EuiSwitch
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.duration.showSuffixLabel"
|
id="indexPatternFieldEditor.duration.showSuffixLabel"
|
||||||
defaultMessage="Show suffix"
|
defaultMessage="Show suffix"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
|
@ -16,7 +16,7 @@ exports[`NumberFormatEditor should render normally 1`] = `
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Documentation"
|
defaultMessage="Documentation"
|
||||||
id="indexPatternManagement.number.documentationLabel"
|
id="indexPatternFieldEditor.number.documentationLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ exports[`NumberFormatEditor should render normally 1`] = `
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
||||||
id="indexPatternManagement.number.numeralLabel"
|
id="indexPatternFieldEditor.number.numeralLabel"
|
||||||
values={
|
values={
|
||||||
Object {
|
Object {
|
||||||
"defaultPattern": <EuiCode>
|
"defaultPattern": <EuiCode>
|
|
@ -36,7 +36,7 @@ export class NumberFormatEditor extends DefaultFormatEditor<NumberFormatEditorPa
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.number.numeralLabel"
|
id="indexPatternFieldEditor.number.numeralLabel"
|
||||||
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
||||||
values={{ defaultPattern: <EuiCode>{defaultPattern}</EuiCode> }}
|
values={{ defaultPattern: <EuiCode>{defaultPattern}</EuiCode> }}
|
||||||
/>
|
/>
|
||||||
|
@ -45,7 +45,7 @@ export class NumberFormatEditor extends DefaultFormatEditor<NumberFormatEditorPa
|
||||||
<span>
|
<span>
|
||||||
<EuiLink target="_blank" href="https://adamwdraper.github.io/Numeral-js/">
|
<EuiLink target="_blank" href="https://adamwdraper.github.io/Numeral-js/">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.number.documentationLabel"
|
id="indexPatternFieldEditor.number.documentationLabel"
|
||||||
defaultMessage="Documentation"
|
defaultMessage="Documentation"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -16,7 +16,7 @@ exports[`PercentFormatEditor should render normally 1`] = `
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Documentation"
|
defaultMessage="Documentation"
|
||||||
id="indexPatternManagement.number.documentationLabel"
|
id="indexPatternFieldEditor.number.documentationLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ exports[`PercentFormatEditor should render normally 1`] = `
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
defaultMessage="Numeral.js format pattern (Default: {defaultPattern})"
|
||||||
id="indexPatternManagement.number.numeralLabel"
|
id="indexPatternFieldEditor.number.numeralLabel"
|
||||||
values={
|
values={
|
||||||
Object {
|
Object {
|
||||||
"defaultPattern": <EuiCode>
|
"defaultPattern": <EuiCode>
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { FieldFormat } from '../../../../../../../../data/public';
|
import { FieldFormat } from 'src/plugins/data/public';
|
||||||
|
|
||||||
import { PercentFormatEditor } from './percent';
|
import { PercentFormatEditor } from './percent';
|
||||||
|
|
|
@ -9,7 +9,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn
|
||||||
"field": "key",
|
"field": "key",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Key"
|
defaultMessage="Key"
|
||||||
id="indexPatternManagement.staticLookup.keyLabel"
|
id="indexPatternFieldEditor.staticLookup.keyLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -18,7 +18,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn
|
||||||
"field": "value",
|
"field": "value",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Value"
|
defaultMessage="Value"
|
||||||
id="indexPatternManagement.staticLookup.valueLabel"
|
id="indexPatternFieldEditor.staticLookup.valueLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -73,7 +73,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Add entry"
|
defaultMessage="Add entry"
|
||||||
id="indexPatternManagement.staticLookup.addEntryButton"
|
id="indexPatternFieldEditor.staticLookup.addEntryButton"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
|
@ -89,7 +89,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Value for unknown key"
|
defaultMessage="Value for unknown key"
|
||||||
id="indexPatternManagement.staticLookup.unknownKeyLabel"
|
id="indexPatternFieldEditor.staticLookup.unknownKeyLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = `
|
||||||
"field": "key",
|
"field": "key",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Key"
|
defaultMessage="Key"
|
||||||
id="indexPatternManagement.staticLookup.keyLabel"
|
id="indexPatternFieldEditor.staticLookup.keyLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -125,7 +125,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = `
|
||||||
"field": "value",
|
"field": "value",
|
||||||
"name": <FormattedMessage
|
"name": <FormattedMessage
|
||||||
defaultMessage="Value"
|
defaultMessage="Value"
|
||||||
id="indexPatternManagement.staticLookup.valueLabel"
|
id="indexPatternFieldEditor.staticLookup.valueLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>,
|
/>,
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
|
@ -174,7 +174,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = `
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Add entry"
|
defaultMessage="Add entry"
|
||||||
id="indexPatternManagement.staticLookup.addEntryButton"
|
id="indexPatternFieldEditor.staticLookup.addEntryButton"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
|
@ -190,7 +190,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = `
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Value for unknown key"
|
defaultMessage="Value for unknown key"
|
||||||
id="indexPatternManagement.staticLookup.unknownKeyLabel"
|
id="indexPatternFieldEditor.staticLookup.unknownKeyLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
|
@ -9,7 +9,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallowWithI18nProvider } from '@kbn/test/jest';
|
import { shallowWithI18nProvider } from '@kbn/test/jest';
|
||||||
import { StaticLookupFormatEditorFormatParams } from './static_lookup';
|
import { StaticLookupFormatEditorFormatParams } from './static_lookup';
|
||||||
import { FieldFormat } from '../../../../../../../../data/public';
|
import { FieldFormat } from 'src/plugins/data/public';
|
||||||
|
|
||||||
import { StaticLookupFormatEditor } from './static_lookup';
|
import { StaticLookupFormatEditor } from './static_lookup';
|
||||||
|
|
|
@ -72,7 +72,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
||||||
field: 'key',
|
field: 'key',
|
||||||
name: (
|
name: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.staticLookup.keyLabel"
|
id="indexPatternFieldEditor.staticLookup.keyLabel"
|
||||||
defaultMessage="Key"
|
defaultMessage="Key"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -96,7 +96,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
||||||
field: 'value',
|
field: 'value',
|
||||||
name: (
|
name: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.staticLookup.valueLabel"
|
id="indexPatternFieldEditor.staticLookup.valueLabel"
|
||||||
defaultMessage="Value"
|
defaultMessage="Value"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -118,15 +118,15 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
name: i18n.translate('indexPatternManagement.staticLookup.actions', {
|
name: i18n.translate('indexPatternFieldEditor.staticLookup.actions', {
|
||||||
defaultMessage: 'actions',
|
defaultMessage: 'actions',
|
||||||
}),
|
}),
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
name: i18n.translate('indexPatternManagement.staticLookup.deleteAria', {
|
name: i18n.translate('indexPatternFieldEditor.staticLookup.deleteAria', {
|
||||||
defaultMessage: 'Delete',
|
defaultMessage: 'Delete',
|
||||||
}),
|
}),
|
||||||
description: i18n.translate('indexPatternManagement.staticLookup.deleteTitle', {
|
description: i18n.translate('indexPatternFieldEditor.staticLookup.deleteTitle', {
|
||||||
defaultMessage: 'Delete entry',
|
defaultMessage: 'Delete entry',
|
||||||
}),
|
}),
|
||||||
onClick: (item: StaticLookupItem) => {
|
onClick: (item: StaticLookupItem) => {
|
||||||
|
@ -148,7 +148,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<EuiButton iconType="plusInCircle" size="s" onClick={this.addLookup}>
|
<EuiButton iconType="plusInCircle" size="s" onClick={this.addLookup}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.staticLookup.addEntryButton"
|
id="indexPatternFieldEditor.staticLookup.addEntryButton"
|
||||||
defaultMessage="Add entry"
|
defaultMessage="Add entry"
|
||||||
/>
|
/>
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
|
@ -156,7 +156,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.staticLookup.unknownKeyLabel"
|
id="indexPatternFieldEditor.staticLookup.unknownKeyLabel"
|
||||||
defaultMessage="Value for unknown key"
|
defaultMessage="Value for unknown key"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor<StaticLookupFo
|
||||||
<EuiFieldText
|
<EuiFieldText
|
||||||
value={formatParams.unknownKeyValue || ''}
|
value={formatParams.unknownKeyValue || ''}
|
||||||
placeholder={i18n.translate(
|
placeholder={i18n.translate(
|
||||||
'indexPatternManagement.staticLookup.leaveBlankPlaceholder',
|
'indexPatternFieldEditor.staticLookup.leaveBlankPlaceholder',
|
||||||
{
|
{
|
||||||
defaultMessage: 'Leave blank to keep value as-is',
|
defaultMessage: 'Leave blank to keep value as-is',
|
||||||
}
|
}
|
|
@ -12,7 +12,7 @@ exports[`StringFormatEditor should render normally 1`] = `
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Transform"
|
defaultMessage="Transform"
|
||||||
id="indexPatternManagement.string.transformLabel"
|
id="indexPatternFieldEditor.string.transformLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
|
@ -47,7 +47,7 @@ export class StringFormatEditor extends DefaultFormatEditor<StringFormatEditorFo
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.string.transformLabel"
|
id="indexPatternFieldEditor.string.transformLabel"
|
||||||
defaultMessage="Transform"
|
defaultMessage="Transform"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
|
@ -12,7 +12,7 @@ exports[`TruncateFormatEditor should render normally 1`] = `
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Field length"
|
defaultMessage="Field length"
|
||||||
id="indexPatternManagement.truncate.lengthLabel"
|
id="indexPatternFieldEditor.truncate.lengthLabel"
|
||||||
values={Object {}}
|
values={Object {}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
|
@ -37,7 +37,7 @@ export class TruncateFormatEditor extends DefaultFormatEditor<TruncateFormatEdit
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.truncate.lengthLabel"
|
id="indexPatternFieldEditor.truncate.lengthLabel"
|
||||||
defaultMessage="Field length"
|
defaultMessage="Field length"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
|
@ -166,14 +166,26 @@ exports[`UrlFormatEditor should render normally 1`] = `
|
||||||
class="euiFormHelpText euiFormRow__text"
|
class="euiFormHelpText euiFormRow__text"
|
||||||
id="generated-id-help"
|
id="generated-id-help"
|
||||||
>
|
>
|
||||||
<button
|
<a
|
||||||
class="euiLink euiLink--primary"
|
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>
|
<span>
|
||||||
URL template help
|
URL template help
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -217,14 +229,26 @@ exports[`UrlFormatEditor should render normally 1`] = `
|
||||||
class="euiFormHelpText euiFormRow__text"
|
class="euiFormHelpText euiFormRow__text"
|
||||||
id="generated-id-help"
|
id="generated-id-help"
|
||||||
>
|
>
|
||||||
<button
|
<a
|
||||||
class="euiLink euiLink--primary"
|
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>
|
<span>
|
||||||
Label template help
|
Label template help
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -10,8 +10,8 @@ import React from 'react';
|
||||||
import { FieldFormat } from 'src/plugins/data/public';
|
import { FieldFormat } from 'src/plugins/data/public';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { UrlFormatEditor } from './url';
|
import { UrlFormatEditor } from './url';
|
||||||
import { coreMock } from '../../../../../../../../../core/public/mocks';
|
import { coreMock } from 'src/core/public/mocks';
|
||||||
import { createKibanaReactContext } from '../../../../../../../../kibana_react/public';
|
import { createKibanaReactContext } from '../../../../../../kibana_react/public';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
@ -76,38 +76,6 @@ describe('UrlFormatEditor', () => {
|
||||||
expect(container).toMatchSnapshot();
|
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 () => {
|
it('should render width and height fields if image', async () => {
|
||||||
const { getByLabelText } = renderWithContext(
|
const { getByLabelText } = renderWithContext(
|
||||||
<UrlFormatEditor
|
<UrlFormatEditor
|
|
@ -22,11 +22,7 @@ import { DefaultFormatEditor, FormatEditorProps } from '../default';
|
||||||
|
|
||||||
import { FormatEditorSamples } from '../../samples';
|
import { FormatEditorSamples } from '../../samples';
|
||||||
|
|
||||||
import { LabelTemplateFlyout } from './label_template_flyout';
|
import { context as contextType } from '../../../../../../kibana_react/public';
|
||||||
|
|
||||||
import { UrlTemplateFlyout } from './url_template_flyout';
|
|
||||||
import type { IndexPatternManagmentContextValue } from '../../../../../../types';
|
|
||||||
import { context as contextType } from '../../../../../../../../kibana_react/public';
|
|
||||||
|
|
||||||
interface OnChangeParam {
|
interface OnChangeParam {
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -59,9 +55,6 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
||||||
> {
|
> {
|
||||||
static contextType = contextType;
|
static contextType = contextType;
|
||||||
static formatId = 'url';
|
static formatId = 'url';
|
||||||
// TODO: @kbn/optimizer can't compile this
|
|
||||||
// declare context: IndexPatternManagmentContextValue;
|
|
||||||
context: IndexPatternManagmentContextValue | undefined;
|
|
||||||
private get sampleIconPath() {
|
private get sampleIconPath() {
|
||||||
const sampleIconPath = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`;
|
const sampleIconPath = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`;
|
||||||
return this.context?.services.http
|
return this.context?.services.http
|
||||||
|
@ -110,32 +103,6 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
||||||
this.onChange(params);
|
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 = () => {
|
renderWidthHeightParameters = () => {
|
||||||
const width = this.sanitizeNumericValue(this.props.formatParams.width);
|
const width = this.sanitizeNumericValue(this.props.formatParams.width);
|
||||||
const height = this.sanitizeNumericValue(this.props.formatParams.height);
|
const height = this.sanitizeNumericValue(this.props.formatParams.height);
|
||||||
|
@ -143,7 +110,7 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage id="indexPatternManagement.url.widthLabel" defaultMessage="Width" />
|
<FormattedMessage id="indexPatternFieldEditor.url.widthLabel" defaultMessage="Width" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<EuiFieldNumber
|
<EuiFieldNumber
|
||||||
|
@ -156,7 +123,10 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage id="indexPatternManagement.url.heightLabel" defaultMessage="Height" />
|
<FormattedMessage
|
||||||
|
id="indexPatternFieldEditor.url.heightLabel"
|
||||||
|
defaultMessage="Height"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<EuiFieldNumber
|
<EuiFieldNumber
|
||||||
|
@ -177,17 +147,9 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<LabelTemplateFlyout
|
|
||||||
isVisible={this.state.showLabelTemplateHelp}
|
|
||||||
onClose={this.hideLabelTemplateHelp}
|
|
||||||
/>
|
|
||||||
<UrlTemplateFlyout
|
|
||||||
isVisible={this.state.showUrlTemplateHelp}
|
|
||||||
onClose={this.hideUrlTemplateHelp}
|
|
||||||
/>
|
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage id="indexPatternManagement.url.typeLabel" defaultMessage="Type" />
|
<FormattedMessage id="indexPatternFieldEditor.url.typeLabel" defaultMessage="Type" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<EuiSelect
|
<EuiSelect
|
||||||
|
@ -209,7 +171,7 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.url.openTabLabel"
|
id="indexPatternFieldEditor.url.openTabLabel"
|
||||||
defaultMessage="Open in a new tab"
|
defaultMessage="Open in a new tab"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -217,9 +179,12 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
||||||
<EuiSwitch
|
<EuiSwitch
|
||||||
label={
|
label={
|
||||||
formatParams.openLinkInCurrentTab ? (
|
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}
|
checked={!formatParams.openLinkInCurrentTab}
|
||||||
|
@ -233,14 +198,17 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.url.urlTemplateLabel"
|
id="indexPatternFieldEditor.url.urlTemplateLabel"
|
||||||
defaultMessage="URL template"
|
defaultMessage="URL template"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
helpText={
|
helpText={
|
||||||
<EuiLink onClick={this.showUrlTemplateHelp}>
|
<EuiLink
|
||||||
|
target="_blank"
|
||||||
|
href={this.context.services.docLinks.links.indexPatterns.fieldFormattersString}
|
||||||
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.url.template.helpLinkText"
|
id="indexPatternFieldEditor.url.template.helpLinkText"
|
||||||
defaultMessage="URL template help"
|
defaultMessage="URL template help"
|
||||||
/>
|
/>
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
|
@ -260,14 +228,17 @@ export class UrlFormatEditor extends DefaultFormatEditor<
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.url.labelTemplateLabel"
|
id="indexPatternFieldEditor.url.labelTemplateLabel"
|
||||||
defaultMessage="Label template"
|
defaultMessage="Label template"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
helpText={
|
helpText={
|
||||||
<EuiLink onClick={this.showLabelTemplateHelp}>
|
<EuiLink
|
||||||
|
target="_blank"
|
||||||
|
href={this.context.services.docLinks.links.indexPatterns.fieldFormattersString}
|
||||||
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="indexPatternManagement.url.labelTemplateHelpText"
|
id="indexPatternFieldEditor.url.labelTemplateHelpText"
|
||||||
defaultMessage="Label template help"
|
defaultMessage="Label template help"
|
||||||
/>
|
/>
|
||||||
</EuiLink>
|
</EuiLink>
|