Field editor to React/EUI (#20245)

This commit is contained in:
Jen Huang 2018-06-29 09:45:54 -07:00 committed by GitHub
parent d29e612ec0
commit 8def9be997
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
138 changed files with 6633 additions and 1962 deletions

View file

@ -32,7 +32,7 @@ const inputFormats = [
{ text: 'Nanoseconds', kind: 'nanoseconds' },
{ text: 'Microseconds', kind: 'microseconds' },
{ text: 'Milliseconds', kind: 'milliseconds' },
DEFAULT_INPUT_FORMAT,
{ ...DEFAULT_INPUT_FORMAT },
{ text: 'Minutes', kind: 'minutes' },
{ text: 'Hours', kind: 'hours' },
{ text: 'Days', kind: 'days' },
@ -42,7 +42,7 @@ const inputFormats = [
];
const DEFAULT_OUTPUT_FORMAT = { text: 'Human Readable', method: 'humanize' };
const outputFormats = [
DEFAULT_OUTPUT_FORMAT,
{ ...DEFAULT_OUTPUT_FORMAT },
{ text: 'Milliseconds', method: 'asMilliseconds' },
{ text: 'Seconds', method: 'asSeconds' },
{ text: 'Minutes', method: 'asMinutes' },

View file

@ -26,6 +26,13 @@ function convertLookupEntriesToMap(lookupEntries) {
export function createStaticLookupFormat(FieldFormat) {
return class StaticLookupFormat extends FieldFormat {
getParamDefaults() {
return {
lookupEntries: [{}],
unknownKeyValue: null,
};
}
_convert(val) {
const lookupEntries = this.param('lookupEntries');
const unknownKeyValue = this.param('unknownKeyValue');

View file

@ -20,11 +20,21 @@
import { asPrettyString } from '../../utils/as_pretty_string';
import { shortenDottedString } from '../../utils/shorten_dotted_string';
const TRANSFORM_OPTIONS = [
{ kind: false, text: '- None -' },
{ kind: 'lower', text: 'Lower Case' },
{ kind: 'upper', text: 'Upper Case' },
{ kind: 'title', text: 'Title Case' },
{ kind: 'short', text: 'Short Dots' },
{ kind: 'base64', text: 'Base64 Decode' }
];
const DEFAULT_TRANSFORM_OPTION = false;
export function createStringFormat(FieldFormat) {
return class StringFormat extends FieldFormat {
getParamDefaults() {
return {
transform: false
transform: DEFAULT_TRANSFORM_OPTION
};
}
@ -66,5 +76,6 @@ export function createStringFormat(FieldFormat) {
'unknown',
'conflict'
];
static transformOptions = TRANSFORM_OPTIONS;
};
}

View file

@ -23,6 +23,13 @@ import { getHighlightHtml } from '../../highlight/highlight_html';
const templateMatchRE = /{{([\s\S]+?)}}/g;
const whitelistUrlSchemes = ['http://', 'https://'];
const URL_TYPES = [
{ kind: 'a', text: 'Link' },
{ kind: 'img', text: 'Image' },
{ kind: 'audio', text: 'Audio' }
];
const DEFAULT_URL_TYPE = 'a';
export function createUrlFormat(FieldFormat) {
class UrlFormat extends FieldFormat {
constructor(params) {
@ -32,7 +39,7 @@ export function createUrlFormat(FieldFormat) {
getParamDefaults() {
return {
type: 'a',
type: DEFAULT_URL_TYPE,
urlTemplate: null,
labelTemplate: null
};
@ -96,6 +103,7 @@ export function createUrlFormat(FieldFormat) {
'unknown',
'conflict'
];
static urlTypes = URL_TYPES;
}
UrlFormat.prototype._convert = {

View file

@ -0,0 +1,12 @@
<kbn-management-app section="kibana">
<kbn-management-indices>
<div class="kuiViewContent">
<kbn-management-index-header
index-pattern="fieldSettings.indexPattern"
></kbn-management-index-header>
</div>
<div id="reactFieldEditor"></div>
</kbn-management-indices>
</kbn-management-app>

View file

@ -17,12 +17,52 @@
* under the License.
*/
import 'ui/field_editor';
import { IndexPatternsFieldProvider } from 'ui/index_patterns/_field';
import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors';
import { KbnUrlProvider } from 'ui/url';
import uiRoutes from 'ui/routes';
import { toastNotifications } from 'ui/notify';
import template from './scripted_field_editor.html';
import template from './create_edit_field.html';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { FieldEditor } from 'ui/field_editor';
const REACT_FIELD_EDITOR_ID = 'reactFieldEditor';
const renderFieldEditor = ($scope, indexPattern, field, {
Field,
getConfig,
$http,
fieldFormatEditors,
redirectAway,
}) => {
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_FIELD_EDITOR_ID);
if (!node) {
return;
}
render(
<FieldEditor
indexPattern={indexPattern}
field={field}
helpers={{
Field,
getConfig,
$http,
fieldFormatEditors,
redirectAway,
}}
/>,
node,
);
});
};
const destroyFieldEditor = () => {
const node = document.getElementById(REACT_FIELD_EDITOR_ID);
node && unmountComponentAtNode(node);
};
uiRoutes
.when('/management/kibana/indices/:indexPatternId/field/:fieldName*', { mode: 'edit' })
@ -49,8 +89,10 @@ uiRoutes
}
},
controllerAs: 'fieldSettings',
controller: function FieldEditorPageController($route, Private, docTitle) {
controller: function FieldEditorPageController($scope, $route, $timeout, $http, Private, docTitle, config) {
const Field = Private(IndexPatternsFieldProvider);
const getConfig = (...args) => config.get(...args);
const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider);
const kbnUrl = Private(KbnUrlProvider);
this.mode = $route.current.mode;
@ -80,8 +122,21 @@ uiRoutes
}
docTitle.change([this.field.name || 'New Scripted Field', this.indexPattern.title]);
this.goBack = function () {
kbnUrl.changeToRoute(this.indexPattern, 'edit');
};
renderFieldEditor($scope, this.indexPattern, this.field, {
Field,
getConfig,
$http,
fieldFormatEditors,
redirectAway: () => {
$timeout(() => {
kbnUrl.changeToRoute(this.indexPattern, this.field.scripted ? 'scriptedFields' : 'indexedFields');
});
}
});
$scope.$on('$destroy', () => {
destroyFieldEditor();
});
}
});

View file

@ -17,4 +17,4 @@
* under the License.
*/
import './samples';
import './create_edit_field';

View file

@ -19,7 +19,7 @@
import _ from 'lodash';
import './index_header';
import './scripted_field_editor';
import './create_edit_field';
import { KbnUrlProvider } from 'ui/url';
import { IndicesEditSectionsProvider } from './edit_sections';
import { fatalError } from 'ui/notify';

View file

@ -1,26 +0,0 @@
<kbn-management-app section="kibana">
<kbn-management-indices>
<div class="kuiViewContent">
<kbn-management-index-header
index-pattern="fieldSettings.indexPattern"
></kbn-management-index-header>
<h2
ng-if="fieldSettings.mode === 'create'"
class="kuiSubTitle kuiVerticalRhythm"
>
Create {{ fieldSettings.field.scripted ? 'Scripted ' : '' }}Field
</h2>
<h2
ng-if="fieldSettings.mode === 'edit'"
class="kuiSubTitle kuiVerticalRhythm"
>
{{ fieldSettings.field.name }}
</h2>
</div>
<field-editor index-pattern="fieldSettings.indexPattern" field="fieldSettings.field"></field-editor>
</kbn-management-indices>
</kbn-management-app>

View file

@ -1,12 +0,0 @@
<select ng-if="detectedType !== 'geo_point'" name="field_type" ng-model="field.type" ng-change="buildRows()">
<option value="string">string</option>
<option value="number">number</option>
<option value="boolean">boolean</option>
<option value="date">date</option>
<option value="geo_point">geo_point</option>
<option value="ip">ip</option>
</select>
<span ng-if="detectedType === 'geo_point'">
geo_point
</span>

View file

@ -0,0 +1,796 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FieldEditor should render create new scripted field correctly 1`] = `
<div>
<eui-text>
<h3>
Create scripted field
</h3>
</eui-text>
<eui-spacer
size="m"
/>
<eui-form>
<scripting-disabled-callOut
isVisible={false}
/>
<scripting-warning-callOut
isVisible={true}
/>
<scripting-help-flyout
isVisible={false}
onClose={[Function]}
/>
<eui-form-row
error="Name is required"
helpText={null}
isInvalid={true}
label="Name"
>
<eui-field-text
data-test-subj="editorFieldName"
isInvalid={true}
onChange={[Function]}
placeholder="New scripted field"
value=""
/>
</eui-form-row>
<eui-form-row
helpText={null}
label="Language"
>
<eui-select
data-test-subj="editorFieldLang"
onChange={[Function]}
options={
Array [
Object {
"text": "painless",
"value": "painless",
},
Object {
"text": "testlang",
"value": "testlang",
},
]
}
value="painless"
/>
</eui-form-row>
<eui-form-row
label="Type"
>
<eui-select
data-test-subj="editorFieldType"
disabled={false}
onChange={[Function]}
options={
Array [
Object {
"text": "number",
"value": "number",
},
Object {
"text": "string",
"value": "string",
},
Object {
"text": "date",
"value": "date",
},
Object {
"text": "boolean",
"value": "boolean",
},
]
}
value="number"
/>
</eui-form-row>
<React.Fragment>
<eui-form-row
helpText={
<span>
Formatting allows you to control the way that specific values are displayed. It can also cause values to be completely changed and prevent highlighting in Discover from working.
</span>
}
label={
<span>
Format
<span>
(Default:
<eui-code>
Test format
</eui-code>
)
</span>
</span>
}
>
<eui-select
data-test-subj="editorSelectedFormatId"
onChange={[Function]}
options={
Array [
Object {
"text": "- Default -",
"value": "",
},
Object {
"text": "Test format",
"value": "test_format",
},
]
}
/>
</eui-form-row>
</React.Fragment>
<eui-form-row
label="Popularity"
>
<eui-field-number
data-test-subj="editorFieldCount"
onChange={[Function]}
/>
</eui-form-row>
<eui-form-row
error="Script is required"
helpText={
<eui-link
onClick={[Function]}
>
Scripting help
</eui-link>
}
isInvalid={true}
label="Script"
>
<eui-textArea
data-test-subj="editorFieldScript"
isInvalid={true}
onChange={[Function]}
/>
</eui-form-row>
<eui-flex-group>
<eui-flex-item
grow={false}
>
<eui-button
data-test-subj="fieldSaveButton"
fill={true}
isDisabled={true}
onClick={[Function]}
>
Create field
</eui-button>
</eui-flex-item>
<eui-flex-item
grow={false}
>
<eui-button-empty
data-test-subj="fieldCancelButton"
onClick={[Function]}
>
Cancel
</eui-button-empty>
</eui-flex-item>
</eui-flex-group>
</eui-form>
<eui-spacer
size="l"
/>
</div>
`;
exports[`FieldEditor should render edit scripted field correctly 1`] = `
<div>
<eui-text>
<h3>
Edit test
</h3>
</eui-text>
<eui-spacer
size="m"
/>
<eui-form>
<scripting-disabled-callOut
isVisible={false}
/>
<scripting-warning-callOut
isVisible={true}
/>
<scripting-help-flyout
isVisible={false}
onClose={[Function]}
/>
<eui-form-row
helpText={null}
label="Language"
>
<eui-select
data-test-subj="editorFieldLang"
onChange={[Function]}
options={
Array [
Object {
"text": "painless",
"value": "painless",
},
Object {
"text": "testlang",
"value": "testlang",
},
]
}
value="painless"
/>
</eui-form-row>
<eui-form-row
label="Type"
>
<eui-select
data-test-subj="editorFieldType"
disabled={false}
onChange={[Function]}
options={
Array [
Object {
"text": "number",
"value": "number",
},
Object {
"text": "string",
"value": "string",
},
Object {
"text": "date",
"value": "date",
},
Object {
"text": "boolean",
"value": "boolean",
},
]
}
value="number"
/>
</eui-form-row>
<React.Fragment>
<eui-form-row
helpText={
<span>
Formatting allows you to control the way that specific values are displayed. It can also cause values to be completely changed and prevent highlighting in Discover from working.
</span>
}
label={
<span>
Format
<span>
(Default:
<eui-code>
Test format
</eui-code>
)
</span>
</span>
}
>
<eui-select
data-test-subj="editorSelectedFormatId"
onChange={[Function]}
options={
Array [
Object {
"text": "- Default -",
"value": "",
},
Object {
"text": "Test format",
"value": "test_format",
},
]
}
/>
</eui-form-row>
</React.Fragment>
<eui-form-row
label="Popularity"
>
<eui-field-number
data-test-subj="editorFieldCount"
onChange={[Function]}
/>
</eui-form-row>
<eui-form-row
error={null}
helpText={
<eui-link
onClick={[Function]}
>
Scripting help
</eui-link>
}
isInvalid={false}
label="Script"
>
<eui-textArea
data-test-subj="editorFieldScript"
isInvalid={false}
onChange={[Function]}
value="doc.test.value"
/>
</eui-form-row>
<eui-flex-group>
<eui-flex-item
grow={false}
>
<eui-button
data-test-subj="fieldSaveButton"
fill={true}
isDisabled={false}
onClick={[Function]}
>
Save field
</eui-button>
</eui-flex-item>
<eui-flex-item
grow={false}
>
<eui-button-empty
data-test-subj="fieldCancelButton"
onClick={[Function]}
>
Cancel
</eui-button-empty>
</eui-flex-item>
<eui-flex-item
grow={false}
>
<eui-button-empty
color="danger"
onClick={[Function]}
>
Delete
</eui-button-empty>
</eui-flex-item>
</eui-flex-group>
</eui-form>
<eui-spacer
size="l"
/>
</div>
`;
exports[`FieldEditor should show conflict field warning 1`] = `
<div>
<eui-text>
<h3>
Create scripted field
</h3>
</eui-text>
<eui-spacer
size="m"
/>
<eui-form>
<scripting-disabled-callOut
isVisible={false}
/>
<scripting-warning-callOut
isVisible={true}
/>
<scripting-help-flyout
isVisible={false}
onClose={[Function]}
/>
<eui-form-row
error={null}
helpText={
<span>
<eui-icon
color="warning"
size="s"
type="alert"
/>
 
<strong>
Mapping Conflict:
</strong>
  You already have a field with the name
<eui-code>
foobar
</eui-code>
. Naming your scripted field with the same name means you won't be able to query both fields at the same time.
</span>
}
isInvalid={false}
label="Name"
>
<eui-field-text
data-test-subj="editorFieldName"
isInvalid={false}
onChange={[Function]}
placeholder="New scripted field"
value="foobar"
/>
</eui-form-row>
<eui-form-row
helpText={null}
label="Language"
>
<eui-select
data-test-subj="editorFieldLang"
onChange={[Function]}
options={
Array [
Object {
"text": "painless",
"value": "painless",
},
Object {
"text": "testlang",
"value": "testlang",
},
]
}
value="painless"
/>
</eui-form-row>
<eui-form-row
label="Type"
>
<eui-select
data-test-subj="editorFieldType"
disabled={false}
onChange={[Function]}
options={
Array [
Object {
"text": "number",
"value": "number",
},
Object {
"text": "string",
"value": "string",
},
Object {
"text": "date",
"value": "date",
},
Object {
"text": "boolean",
"value": "boolean",
},
]
}
value="number"
/>
</eui-form-row>
<React.Fragment>
<eui-form-row
helpText={
<span>
Formatting allows you to control the way that specific values are displayed. It can also cause values to be completely changed and prevent highlighting in Discover from working.
</span>
}
label={
<span>
Format
<span>
(Default:
<eui-code>
Test format
</eui-code>
)
</span>
</span>
}
>
<eui-select
data-test-subj="editorSelectedFormatId"
onChange={[Function]}
options={
Array [
Object {
"text": "- Default -",
"value": "",
},
Object {
"text": "Test format",
"value": "test_format",
},
]
}
/>
</eui-form-row>
</React.Fragment>
<eui-form-row
label="Popularity"
>
<eui-field-number
data-test-subj="editorFieldCount"
onChange={[Function]}
/>
</eui-form-row>
<eui-form-row
error="Script is required"
helpText={
<eui-link
onClick={[Function]}
>
Scripting help
</eui-link>
}
isInvalid={true}
label="Script"
>
<eui-textArea
data-test-subj="editorFieldScript"
isInvalid={true}
onChange={[Function]}
/>
</eui-form-row>
<eui-flex-group>
<eui-flex-item
grow={false}
>
<eui-button
data-test-subj="fieldSaveButton"
fill={true}
isDisabled={true}
onClick={[Function]}
>
Create field
</eui-button>
</eui-flex-item>
<eui-flex-item
grow={false}
>
<eui-button-empty
data-test-subj="fieldCancelButton"
onClick={[Function]}
>
Cancel
</eui-button-empty>
</eui-flex-item>
</eui-flex-group>
</eui-form>
<eui-spacer
size="l"
/>
</div>
`;
exports[`FieldEditor should show deprecated lang warning 1`] = `
<div>
<eui-text>
<h3>
Edit test
</h3>
</eui-text>
<eui-spacer
size="m"
/>
<eui-form>
<scripting-disabled-callOut
isVisible={false}
/>
<scripting-warning-callOut
isVisible={true}
/>
<scripting-help-flyout
isVisible={false}
onClose={[Function]}
/>
<eui-form-row
helpText={
<span>
<eui-icon
color="warning"
size="s"
type="alert"
/>
 
<strong>
Deprecation Warning:
</strong>
 
<eui-code>
testlang
</eui-code>
is deprecated and support will be removed in the next major version of Kibana and Elasticsearch. We recommend using
<eui-link
href="(docLink for scriptedFields.painless)"
target="_window"
>
Painless
</eui-link>
for new scripted fields.
</span>
}
label="Language"
>
<eui-select
data-test-subj="editorFieldLang"
onChange={[Function]}
options={
Array [
Object {
"text": "painless",
"value": "painless",
},
Object {
"text": "testlang",
"value": "testlang",
},
]
}
value="testlang"
/>
</eui-form-row>
<eui-form-row
label="Type"
>
<eui-select
data-test-subj="editorFieldType"
disabled={false}
onChange={[Function]}
options={
Array [
Object {
"text": "string",
"value": "string",
},
Object {
"text": "number",
"value": "number",
},
Object {
"text": "date",
"value": "date",
},
Object {
"text": "ip",
"value": "ip",
},
Object {
"text": "boolean",
"value": "boolean",
},
Object {
"text": "geo_point",
"value": "geo_point",
},
Object {
"text": "geo_shape",
"value": "geo_shape",
},
Object {
"text": "attachment",
"value": "attachment",
},
Object {
"text": "murmur3",
"value": "murmur3",
},
Object {
"text": "_source",
"value": "_source",
},
Object {
"text": "unknown",
"value": "unknown",
},
Object {
"text": "conflict",
"value": "conflict",
},
]
}
value="number"
/>
</eui-form-row>
<React.Fragment>
<eui-form-row
helpText={
<span>
Formatting allows you to control the way that specific values are displayed. It can also cause values to be completely changed and prevent highlighting in Discover from working.
</span>
}
label={
<span>
Format
<span>
(Default:
<eui-code>
Test format
</eui-code>
)
</span>
</span>
}
>
<eui-select
data-test-subj="editorSelectedFormatId"
onChange={[Function]}
options={
Array [
Object {
"text": "- Default -",
"value": "",
},
Object {
"text": "Test format",
"value": "test_format",
},
]
}
/>
</eui-form-row>
</React.Fragment>
<eui-form-row
label="Popularity"
>
<eui-field-number
data-test-subj="editorFieldCount"
onChange={[Function]}
/>
</eui-form-row>
<eui-form-row
error={null}
helpText={
<eui-link
onClick={[Function]}
>
Scripting help
</eui-link>
}
isInvalid={false}
label="Script"
>
<eui-textArea
data-test-subj="editorFieldScript"
isInvalid={false}
onChange={[Function]}
value="doc.test.value"
/>
</eui-form-row>
<eui-flex-group>
<eui-flex-item
grow={false}
>
<eui-button
data-test-subj="fieldSaveButton"
fill={true}
isDisabled={false}
onClick={[Function]}
>
Save field
</eui-button>
</eui-flex-item>
<eui-flex-item
grow={false}
>
<eui-button-empty
data-test-subj="fieldCancelButton"
onClick={[Function]}
>
Cancel
</eui-button-empty>
</eui-flex-item>
<eui-flex-item
grow={false}
>
<eui-button-empty
color="danger"
onClick={[Function]}
>
Delete
</eui-button-empty>
</eui-flex-item>
</eui-flex-group>
</eui-form>
<eui-spacer
size="l"
/>
</div>
`;

View file

@ -1,226 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import $ from 'jquery';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import { IndexPatternsFieldProvider } from '../../index_patterns/_field';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import _ from 'lodash';
describe('FieldEditor directive', function () {
let Field;
let $rootScope;
let compile;
let $scope;
let $el;
let $httpBackend;
let getScriptedLangsResponse;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($compile, $injector, Private) {
$httpBackend = $injector.get('$httpBackend');
getScriptedLangsResponse = $httpBackend.when('GET', '/api/kibana/scripts/languages');
getScriptedLangsResponse.respond(['expression', 'painless']);
$rootScope = $injector.get('$rootScope');
Field = Private(IndexPatternsFieldProvider);
$rootScope.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
$rootScope.indexPattern.stubSetFieldFormat('time', 'string', { foo: 1, bar: 2 });
$rootScope.field = $rootScope.indexPattern.fields.byName.time;
compile = function () {
$el = $compile($('<field-editor field="field" index-pattern="indexPattern">'))($rootScope);
$scope = $el.data('$isolateScope');
};
}));
describe('$scope', function () {
it('is isolated', function () {
compile();
expect($scope.parent == null).to.be.ok();
expect($scope).to.not.be($rootScope);
});
it('exposes $scope.editor, a controller for the editor', function () {
compile();
const editor = $scope.editor;
expect(editor).to.be.an('object');
});
});
describe('$scope.editor', function () {
let editor;
beforeEach(function () {
compile();
editor = $scope.editor;
});
it('exposes editor.indexPattern', function () {
expect(editor.indexPattern).to.be($rootScope.indexPattern);
});
it('exposes editor.field', function () {
expect(editor.field).to.be.an('object');
});
describe('editor.field', function () {
let field;
let actual;
beforeEach(function () {
field = editor.field;
actual = $rootScope.field;
});
it('looks like the field from the index pattern, but isn\'t', function () {
expect(field).to.not.be(actual);
expect(field).to.not.be.a(Field);
expect(field.name).to.be(actual.name);
expect(field.type).to.be(actual.type);
expect(field.scripted).to.be(actual.scripted);
expect(field.script).to.be(actual.script);
});
it('reflects changes to the index patterns field', function () {
const a = {};
const b = {};
actual.script = a;
expect(field.script).to.be(a);
actual.script = b;
expect(field.script).to.be(b);
});
it('is fully mutable, unlike the index patterns field', function () {
const origName = actual.name;
actual.name = 'john';
expect(actual.name).to.not.be('john');
expect(actual.name).to.be(origName);
expect(field.name).to.be(origName);
field.name = 'john';
expect(field.name).to.be('john');
expect(actual.name).to.be(origName);
});
});
it('exposes editor.formatParams', function () {
expect(editor).to.have.property('formatParams');
expect(editor.field.format.params()).to.eql(editor.formatParams);
});
describe('editor.formatParams', function () {
it('initializes with all of the formats current params', function () {
// rebuild the editor
compile();
editor = $scope.editor;
expect(editor.formatParams).to.have.property('foo', 1);
expect(editor.formatParams).to.have.property('bar', 2);
});
it('updates the fields format when changed', function () {
$rootScope.$apply(); // initial apply in order to pick up change
editor.formatParams.foo = 200;
$rootScope.$apply();
expect(editor.field.format.param('foo')).to.be(200);
});
});
describe('scripted fields', function () {
let editor;
let field;
beforeEach(function () {
$rootScope.field = $rootScope.indexPattern.fields.byName['script string'];
compile();
editor = $scope.editor;
field = editor.field;
});
it('has a scripted flag set to true', function () {
expect(field.scripted).to.be(true);
});
it('contains a lang param', function () {
expect(field).to.have.property('lang');
expect(field.lang).to.be('expression');
});
it('limits lang options to "expression" and "painless"', function () {
getScriptedLangsResponse
.respond(['expression', 'painless', 'groovy']);
$httpBackend.flush();
expect(editor.scriptingLangs).to.eql(['painless']);
});
it('provides specific type when language is painless', function () {
$rootScope.$apply();
expect(editor.fieldTypes).to.have.length(1);
expect(editor.fieldTypes[0]).to.be('number');
editor.field.lang = 'painless';
$rootScope.$apply();
expect(editor.fieldTypes).to.have.length(4);
expect(_.isEqual(editor.fieldTypes, ['number', 'string', 'date', 'boolean'])).to.be.ok();
});
it('provides all kibana types when language is groovy (only possible in 5.x)', function () {
$rootScope.$apply();
expect(editor.fieldTypes).to.have.length(1);
expect(editor.fieldTypes[0]).to.be('number');
editor.field.lang = 'groovy';
$rootScope.$apply();
expect(editor.fieldTypes).to.contain('number');
expect(editor.fieldTypes).to.contain('string');
expect(editor.fieldTypes).to.contain('geo_point');
expect(editor.fieldTypes).to.contain('ip');
expect(editor.fieldTypes).to.contain('attachment');
expect(editor.fieldTypes).to.not.contain('text');
expect(editor.fieldTypes).to.not.contain('keyword');
});
it('updates formatter options based on field type', function () {
field.lang = 'painless';
$rootScope.$apply();
expect(editor.field.type).to.be('string');
const stringFormats = editor.fieldFormatTypes;
field.type = 'date';
$rootScope.$apply();
expect(editor.fieldFormatTypes).to.not.be(stringFormats);
});
});
});
});

View file

@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FieldFormatEditor should render normally 1`] = `
<React.Fragment>
<TestEditor
fieldType="number"
format={Object {}}
formatParams={Object {}}
onChange={[Function]}
onError={[Function]}
/>
</React.Fragment>
`;
exports[`FieldFormatEditor should render nothing if there is no editor for the format 1`] = `<React.Fragment />`;

View file

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BytesFormatEditor should render normally 1`] = `
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<span>
<EuiLink
color="primary"
href="https://adamwdraper.github.io/Numeral-js/"
target="_window"
type="button"
>
Documentation
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
</span>
}
isInvalid={false}
label={
<span>
Numeral.js format pattern (Default:
<EuiCode>
0,0.[000]b
</EuiCode>
)
</span>
}
>
<EuiFieldText
compressed={false}
fullWidth={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
placeholder="0,0.[000]b"
/>
</EuiFormRow>
<FormatEditorSamples
samples={
Array [
Object {
"input": 256,
"output": 512,
},
Object {
"input": 1024,
"output": 2048,
},
Object {
"input": 5150000,
"output": 10300000,
},
Object {
"input": 1990000000,
"output": 3980000000,
},
]
}
/>
</React.Fragment>
`;

View file

@ -17,16 +17,17 @@
* under the License.
*/
import numeralTemplate from './numeral.html';
import { NumberFormatEditor } from '../number';
export function bytesEditor() {
export class BytesFormatEditor extends NumberFormatEditor {
static formatId = 'bytes';
return {
formatId: 'bytes',
template: numeralTemplate,
controllerAs: 'cntrl',
controller: function () {
this.sampleInputs = [1024, 5150000, 1990000000];
}
};
constructor(props) {
super(props);
this.state = {
...this.state,
sampleInputs: [256, 1024, 5150000, 1990000000],
};
}
}

View file

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { BytesFormatEditor } from './bytes';
const fieldType = 'number';
const format = {
getConverterFor: jest.fn().mockImplementation(() => (input) => input * 2),
getParamDefaults: jest.fn().mockImplementation(() => {
return { pattern: '0,0.[000]b' };
}),
};
const formatParams = {};
const onChange = jest.fn();
const onError = jest.fn();
describe('BytesFormatEditor', () => {
it('should render normally', async () => {
const component = shallow(
<BytesFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { durationEditor } from './duration';
export { BytesFormatEditor } from './bytes';

View file

@ -0,0 +1,227 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ColorFormatEditor should render multiple colors 1`] = `
<React.Fragment>
<EuiBasicTable
columns={
Array [
Object {
"field": "regex",
"name": "Pattern (regular expression)",
"render": [Function],
},
Object {
"field": "text",
"name": "Text color",
"render": [Function],
},
Object {
"field": "background",
"name": "Background color",
"render": [Function],
},
Object {
"name": "Example",
"render": [Function],
},
Object {
"actions": Array [
Object {
"available": [Function],
"color": "danger",
"description": "Delete color format",
"icon": "trash",
"name": "Delete",
"onClick": [Function],
"type": "icon",
},
],
},
]
}
items={
Array [
Object {
"background": "#ffffff",
"index": 0,
"range": "-Infinity:Infinity",
"regex": "<insert regex>",
"text": "#000000",
},
Object {
"background": "#ffffff",
"index": 1,
"range": "-Infinity:Infinity",
"regex": "<insert regex>",
"text": "#000000",
},
]
}
noItemsMessage="No items found"
responsive={true}
/>
<EuiSpacer
size="m"
/>
<EuiButton
color="primary"
fill={false}
iconSide="left"
iconType="plusInCircle"
onClick={[Function]}
size="s"
type="button"
>
Add color
</EuiButton>
<EuiSpacer
size="l"
/>
</React.Fragment>
`;
exports[`ColorFormatEditor should render other type normally (range field) 1`] = `
<React.Fragment>
<EuiBasicTable
columns={
Array [
Object {
"field": "range",
"name": "Range (min:max)",
"render": [Function],
},
Object {
"field": "text",
"name": "Text color",
"render": [Function],
},
Object {
"field": "background",
"name": "Background color",
"render": [Function],
},
Object {
"name": "Example",
"render": [Function],
},
Object {
"actions": Array [
Object {
"available": [Function],
"color": "danger",
"description": "Delete color format",
"icon": "trash",
"name": "Delete",
"onClick": [Function],
"type": "icon",
},
],
},
]
}
items={
Array [
Object {
"background": "#ffffff",
"index": 0,
"range": "-Infinity:Infinity",
"regex": "<insert regex>",
"text": "#000000",
},
]
}
noItemsMessage="No items found"
responsive={true}
/>
<EuiSpacer
size="m"
/>
<EuiButton
color="primary"
fill={false}
iconSide="left"
iconType="plusInCircle"
onClick={[Function]}
size="s"
type="button"
>
Add color
</EuiButton>
<EuiSpacer
size="l"
/>
</React.Fragment>
`;
exports[`ColorFormatEditor should render string type normally (regex field) 1`] = `
<React.Fragment>
<EuiBasicTable
columns={
Array [
Object {
"field": "regex",
"name": "Pattern (regular expression)",
"render": [Function],
},
Object {
"field": "text",
"name": "Text color",
"render": [Function],
},
Object {
"field": "background",
"name": "Background color",
"render": [Function],
},
Object {
"name": "Example",
"render": [Function],
},
Object {
"actions": Array [
Object {
"available": [Function],
"color": "danger",
"description": "Delete color format",
"icon": "trash",
"name": "Delete",
"onClick": [Function],
"type": "icon",
},
],
},
]
}
items={
Array [
Object {
"background": "#ffffff",
"index": 0,
"range": "-Infinity:Infinity",
"regex": "<insert regex>",
"text": "#000000",
},
]
}
noItemsMessage="No items found"
responsive={true}
/>
<EuiSpacer
size="m"
/>
<EuiButton
color="primary"
fill={false}
iconSide="left"
iconType="plusInCircle"
onClick={[Function]}
size="s"
type="button"
>
Add color
</EuiButton>
<EuiSpacer
size="l"
/>
</React.Fragment>
`;

View file

@ -0,0 +1,199 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import {
EuiBasicTable,
EuiButton,
EuiColorPicker,
EuiFieldText,
EuiSpacer,
} from '@elastic/eui';
import {
DefaultFormatEditor
} from '../default';
import { DEFAULT_COLOR } from '../../../../../../../core_plugins/kibana/common/field_formats/types/color_default';
export class ColorFormatEditor extends DefaultFormatEditor {
static formatId = 'color';
constructor(props) {
super(props);
this.onChange({
fieldType: props.fieldType,
});
}
onColorChange = (newColorParams, index) => {
const colors = [...this.props.formatParams.colors];
colors[index] = {
...colors[index],
...newColorParams,
};
this.onChange({
colors,
});
}
addColor = () => {
const colors = [...this.props.formatParams.colors];
this.onChange({
colors: [
...colors,
{ ...DEFAULT_COLOR }
],
});
}
removeColor = (index) => {
const colors = [...this.props.formatParams.colors];
colors.splice(index, 1);
this.onChange({
colors,
});
}
render() {
const { formatParams, fieldType } = this.props;
const items = formatParams.colors && formatParams.colors.length && formatParams.colors.map((color, index) => {
return {
...color,
index,
};
}) || [];
const columns = [
fieldType === 'string' ? {
field: 'regex',
name: 'Pattern (regular expression)',
render: (value, item) => {
return (
<EuiFieldText
value={value}
onChange={(e) => {
this.onColorChange({
regex: e.target.value,
}, item.index);
}}
/>
);
}
} : {
field: 'range',
name: 'Range (min:max)',
render: (value, item) => {
return (
<EuiFieldText
value={value}
onChange={(e) => {
this.onColorChange({
range: e.target.value,
}, item.index);
}}
/>
);
}
},
{
field: 'text',
name: 'Text color',
render: (color, item) => {
return (
<EuiColorPicker
color={color}
onChange={(newColor) => {
this.onColorChange({
text: newColor,
}, item.index);
}}
/>
);
}
},
{
field: 'background',
name: 'Background color',
render: (color, item) => {
return (
<EuiColorPicker
color={color}
onChange={(newColor) => {
this.onColorChange({
background: newColor,
}, item.index);
}}
/>
);
}
},
{
name: 'Example',
render: (item) => {
return (
<div
style={{
background: item.background,
color: item.text
}}
>
123456
</div>
);
}
},
{
actions: [
{
name: 'Delete',
description: 'Delete color format',
onClick: (item) => {
this.removeColor(item.index);
},
type: 'icon',
icon: 'trash',
color: 'danger',
available: () => items.length > 1
}
],
}
];
return (
<Fragment>
<EuiBasicTable
items={items}
columns={columns}
/>
<EuiSpacer size="m" />
<EuiButton
iconType="plusInCircle"
size="s"
onClick={this.addColor}
>
Add color
</EuiButton>
<EuiSpacer size="l" />
</Fragment>
);
}
}

View file

@ -0,0 +1,79 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { ColorFormatEditor } from './color';
import { DEFAULT_COLOR } from '../../../../../../../core_plugins/kibana/common/field_formats/types/color_default';
const fieldType = 'string';
const format = {
getConverterFor: jest.fn(),
};
const formatParams = {
colors: [{ ...DEFAULT_COLOR }],
};
const onChange = jest.fn();
const onError = jest.fn();
describe('ColorFormatEditor', () => {
it('should render string type normally (regex field)', async () => {
const component = shallow(
<ColorFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
it('should render other type normally (range field)', async () => {
const component = shallow(
<ColorFormatEditor
fieldType={'number'}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
it('should render multiple colors', async () => {
const component = shallow(
<ColorFormatEditor
fieldType={fieldType}
format={format}
formatParams={{ colors: [...formatParams.colors, ...formatParams.colors] }}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { colorEditor } from './color';
export { ColorFormatEditor } from './color';

View file

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DateFormatEditor should render normally 1`] = `
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<span>
<EuiLink
color="primary"
href="http://momentjs.com/"
target="_window"
type="button"
>
Documentation
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
</span>
}
isInvalid={false}
label={
<span>
Moment.js format pattern (Default:
<EuiCode>
MMMM Do YYYY, HH:mm:ss.SSS
</EuiCode>
)
</span>
}
>
<EuiFieldText
compressed={false}
data-test-subj="dateEditorPattern"
fullWidth={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
placeholder="MMMM Do YYYY, HH:mm:ss.SSS"
/>
</EuiFormRow>
<FormatEditorSamples
samples={
Array [
Object {
"input": 1529097045190,
"output": "converted date for 1529097045190",
},
Object {
"input": 1514793600000,
"output": "converted date for 1514793600000",
},
Object {
"input": 1546329599999,
"output": "converted date for 1546329599999",
},
]
}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,90 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import moment from 'moment';
import {
EuiCode,
EuiFieldText,
EuiFormRow,
EuiIcon,
EuiLink,
} from '@elastic/eui';
import {
DefaultFormatEditor
} from '../default';
import {
FormatEditorSamples
} from '../../samples';
export class DateFormatEditor extends DefaultFormatEditor {
static formatId = 'date';
constructor(props) {
super(props);
this.state.sampleInputs = [
Date.now(),
moment().startOf('year').valueOf(),
moment().endOf('year').valueOf()
];
}
render() {
const { format, formatParams } = this.props;
const { error, samples } = this.state;
const defaultPattern = format.getParamDefaults().pattern;
return (
<Fragment>
<EuiFormRow
label={
<span>
Moment.js format pattern (Default: <EuiCode>{defaultPattern}</EuiCode>)
</span>
}
isInvalid={!!error}
error={error}
helpText={
<span>
<EuiLink target="_window" href="http://momentjs.com/">
Documentation <EuiIcon type="link" />
</EuiLink>
</span>
}
>
<EuiFieldText
data-test-subj="dateEditorPattern"
value={formatParams.pattern}
placeholder={defaultPattern}
onChange={(e) => {
this.onChange({ pattern: e.target.value });
}}
isInvalid={!!error}
/>
</EuiFormRow>
<FormatEditorSamples
samples={samples}
/>
</Fragment>
);
}
}

View file

@ -0,0 +1,58 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { DateFormatEditor } from './date';
const fieldType = 'date';
const format = {
getConverterFor: jest.fn().mockImplementation(() => (input) => `converted date for ${input}`),
getParamDefaults: jest.fn().mockImplementation(() => {
return { pattern: 'MMMM Do YYYY, HH:mm:ss.SSS' };
}),
};
const formatParams = {};
const onChange = jest.fn();
const onError = jest.fn();
describe('DateFormatEditor', () => {
it('should render normally', async () => {
const component = shallow(
<DateFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
// Date editor samples uses changing values - Date.now() - so we
// hardcode samples to avoid ever-changing snapshots
component.setState({
sampleInputs: [1529097045190, 1514793600000, 1546329599999],
});
component.instance().forceUpdate();
component.update();
expect(component).toMatchSnapshot();
});
});

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { stringEditor } from './string';
export { DateFormatEditor } from './date';

View file

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DefaultFormatEditor should render nothing 1`] = `""`;

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
convertSampleInput
} from '../../../../lib';
export class DefaultFormatEditor extends PureComponent {
static propTypes = {
fieldType: PropTypes.string.isRequired,
format: PropTypes.object.isRequired,
formatParams: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onError: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
sampleInputs: [],
sampleConverterType: 'text',
error: null,
samples: [],
};
}
static getDerivedStateFromProps(nextProps, state) {
const { format, formatParams, onError } = nextProps;
const { sampleInputsByType, sampleInputs, sampleConverterType } = state;
const converter = format.getConverterFor(sampleConverterType);
const type = typeof sampleInputsByType === 'object' && formatParams.type;
const inputs = type ? sampleInputsByType[formatParams.type] || [] : sampleInputs;
const output = convertSampleInput(converter, inputs);
onError(output.error);
return output;
}
onChange = (newParams = {}) => {
const { onChange, formatParams } = this.props;
onChange({
...formatParams,
...newParams
});
};
render() {
return null;
}
}

View file

@ -0,0 +1,84 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { DefaultFormatEditor } from './default';
const fieldType = 'number';
const format = {
getConverterFor: jest.fn().mockImplementation(() => () => {}),
};
const formatParams = {};
const onChange = jest.fn();
const onError = jest.fn();
describe('DefaultFormatEditor', () => {
it('should render nothing', async () => {
const component = shallow(
<DefaultFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(format.getConverterFor).toBeCalled();
expect(onError).toBeCalled();
expect(component).toMatchSnapshot();
});
it('should call prop onChange()', async () => {
const component = shallow(
<DefaultFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
component.instance().onChange();
expect(onChange).toBeCalled();
});
it('should call prop onError() if converter throws an error', async () => {
const newFormat = {
getConverterFor: jest.fn().mockImplementation(() => () => {
throw ({ message: 'Test error message' });
}),
};
shallow(
<DefaultFormatEditor
fieldType={fieldType}
format={newFormat}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(onError).toBeCalled();
});
});

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { DefaultFormatEditor } from './default';

View file

@ -0,0 +1,219 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DurationFormatEditor should render human readable output normally 1`] = `
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Input format"
>
<EuiSelect
compressed={false}
fullWidth={false}
hasNoInitialSelection={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"text": "Seconds",
"value": "seconds",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Output format"
>
<EuiSelect
compressed={false}
fullWidth={false}
hasNoInitialSelection={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"text": "Human Readable",
"value": "humanize",
},
Object {
"text": "Minutes",
"value": "asMinutes",
},
]
}
/>
</EuiFormRow>
<FormatEditorSamples
samples={
Array [
Object {
"input": -123,
"output": "converted duration for -123",
},
Object {
"input": 1,
"output": "converted duration for 1",
},
Object {
"input": 12,
"output": "converted duration for 12",
},
Object {
"input": 123,
"output": "converted duration for 123",
},
Object {
"input": 658,
"output": "converted duration for 658",
},
Object {
"input": 1988,
"output": "converted duration for 1988",
},
Object {
"input": 3857,
"output": "converted duration for 3857",
},
Object {
"input": 123292,
"output": "converted duration for 123292",
},
Object {
"input": 923528271,
"output": "converted duration for 923528271",
},
]
}
/>
</React.Fragment>
`;
exports[`DurationFormatEditor should render non-human readable output normally 1`] = `
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Input format"
>
<EuiSelect
compressed={false}
fullWidth={false}
hasNoInitialSelection={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"text": "Seconds",
"value": "seconds",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Output format"
>
<EuiSelect
compressed={false}
fullWidth={false}
hasNoInitialSelection={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"text": "Human Readable",
"value": "humanize",
},
Object {
"text": "Minutes",
"value": "asMinutes",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Decimal places"
>
<EuiFieldNumber
compressed={false}
fullWidth={false}
isInvalid={false}
isLoading={false}
max={20}
min={0}
onChange={[Function]}
/>
</EuiFormRow>
<FormatEditorSamples
samples={
Array [
Object {
"input": -123,
"output": "converted duration for -123",
},
Object {
"input": 1,
"output": "converted duration for 1",
},
Object {
"input": 12,
"output": "converted duration for 12",
},
Object {
"input": 123,
"output": "converted duration for 123",
},
Object {
"input": 658,
"output": "converted duration for 658",
},
Object {
"input": 1988,
"output": "converted duration for 1988",
},
Object {
"input": 3857,
"output": "converted duration for 3857",
},
Object {
"input": 123292,
"output": "converted duration for 123292",
},
Object {
"input": 923528271,
"output": "converted duration for 923528271",
},
]
}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,133 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import {
EuiFieldNumber,
EuiFormRow,
EuiSelect,
} from '@elastic/eui';
import {
DefaultFormatEditor
} from '../default';
import {
FormatEditorSamples
} from '../../samples';
export class DurationFormatEditor extends DefaultFormatEditor {
static formatId = 'duration';
constructor(props) {
super(props);
this.state.sampleInputs = [-123, 1, 12, 123, 658, 1988, 3857, 123292, 923528271];
this.state.hasDecimalError = false;
}
static getDerivedStateFromProps(nextProps, state) {
const output = super.getDerivedStateFromProps(nextProps, state);
let error = null;
if(!nextProps.format.isHuman() && nextProps.formatParams.outputPrecision > 20) {
error = 'Decimal places must be between 0 and 20';
nextProps.onError(error);
return {
...output,
error,
hasDecimalError: true,
};
}
return {
...output,
hasDecimalError: false,
};
}
render() {
const { format, formatParams } = this.props;
const { error, samples, hasDecimalError } = this.state;
return (
<Fragment>
<EuiFormRow
label="Input format"
isInvalid={!!error}
error={hasDecimalError ? null : error}
>
<EuiSelect
value={formatParams.inputFormat}
options={format.type.inputFormats.map(format => {
return {
value: format.kind,
text: format.text,
};
})}
onChange={(e) => {
this.onChange({ inputFormat: e.target.value });
}}
isInvalid={!!error}
/>
</EuiFormRow>
<EuiFormRow
label="Output format"
isInvalid={!!error}
>
<EuiSelect
value={formatParams.outputFormat}
options={format.type.outputFormats.map(format => {
return {
value: format.method,
text: format.text,
};
})}
onChange={(e) => {
this.onChange({ outputFormat: e.target.value });
}}
isInvalid={!!error}
/>
</EuiFormRow>
{
!format.isHuman() ? (
<EuiFormRow
label="Decimal places"
isInvalid={!!error}
error={hasDecimalError ? error : null}
>
<EuiFieldNumber
value={formatParams.outputPrecision}
min={0}
max={20}
onChange={(e) => {
this.onChange({ outputPrecision: e.target.value ? Number(e.target.value) : null });
}}
isInvalid={!!error}
/>
</EuiFormRow>
) : null
}
<FormatEditorSamples
samples={samples}
/>
</Fragment>
);
}
}

View file

@ -0,0 +1,96 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { DurationFormatEditor } from './duration';
const fieldType = 'number';
const format = {
getConverterFor: jest.fn().mockImplementation(() => (input) => `converted duration for ${input}`),
getParamDefaults: jest.fn().mockImplementation(() => {
return {
inputFormat: 'seconds',
outputFormat: 'humanize',
outputPrecision: 10,
};
}),
isHuman: () => true,
type: {
inputFormats: [
{
text: 'Seconds',
kind: 'seconds'
},
],
outputFormats: [
{
text: 'Human Readable',
method: 'humanize',
},
{
text: 'Minutes',
method: 'asMinutes',
},
],
}
};
const formatParams = {};
const onChange = jest.fn();
const onError = jest.fn();
describe('DurationFormatEditor', () => {
it('should render human readable output normally', async () => {
const component = shallow(
<DurationFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
it('should render non-human readable output normally', async () => {
const newFormat = {
...format,
getParamDefaults: jest.fn().mockImplementation(() => {
return {
inputFormat: 'seconds',
outputFormat: 'asMinutes',
outputPrecision: 10,
};
}),
isHuman: () => false,
};
const component = shallow(
<DurationFormatEditor
fieldType={fieldType}
format={newFormat}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { DurationFormatEditor } from './duration';

View file

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NumberFormatEditor should render normally 1`] = `
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<span>
<EuiLink
color="primary"
href="https://adamwdraper.github.io/Numeral-js/"
target="_window"
type="button"
>
Documentation
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
</span>
}
isInvalid={false}
label={
<span>
Numeral.js format pattern (Default:
<EuiCode>
0,0.[000]
</EuiCode>
)
</span>
}
>
<EuiFieldText
compressed={false}
fullWidth={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
placeholder="0,0.[000]"
/>
</EuiFormRow>
<FormatEditorSamples
samples={
Array [
Object {
"input": 10000,
"output": 20000,
},
Object {
"input": 12.345678,
"output": 24.691356,
},
Object {
"input": -1,
"output": -2,
},
Object {
"input": -999,
"output": -1998,
},
Object {
"input": 0.52,
"output": 1.04,
},
]
}
/>
</React.Fragment>
`;

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { dateEditor } from './date';
export { NumberFormatEditor } from './number';

View file

@ -0,0 +1,84 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import {
EuiCode,
EuiFieldText,
EuiFormRow,
EuiIcon,
EuiLink,
} from '@elastic/eui';
import {
DefaultFormatEditor
} from '../default';
import {
FormatEditorSamples
} from '../../samples';
export class NumberFormatEditor extends DefaultFormatEditor {
static formatId = 'number';
constructor(props) {
super(props);
this.state.sampleInputs = [10000, 12.345678, -1, -999, 0.52];
}
render() {
const { format, formatParams } = this.props;
const { error, samples } = this.state;
const defaultPattern = format.getParamDefaults().pattern;
return (
<Fragment>
<EuiFormRow
label={
<span>
Numeral.js format pattern (Default: <EuiCode>{defaultPattern}</EuiCode>)
</span>
}
helpText={
<span>
<EuiLink target="_window" href="https://adamwdraper.github.io/Numeral-js/">
Documentation <EuiIcon type="link" />
</EuiLink>
</span>
}
isInvalid={!!error}
error={error}
>
<EuiFieldText
value={formatParams.pattern}
placeholder={defaultPattern}
onChange={(e) => {
this.onChange({ pattern: e.target.value });
}}
isInvalid={!!error}
/>
</EuiFormRow>
<FormatEditorSamples
samples={samples}
/>
</Fragment>
);
}
}

View file

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { NumberFormatEditor } from './number';
const fieldType = 'number';
const format = {
getConverterFor: jest.fn().mockImplementation(() => (input) => input * 2),
getParamDefaults: jest.fn().mockImplementation(() => {
return { pattern: '0,0.[000]' };
}),
};
const formatParams = {};
const onChange = jest.fn();
const onError = jest.fn();
describe('NumberFormatEditor', () => {
it('should render normally', async () => {
const component = shallow(
<NumberFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PercentFormatEditor should render normally 1`] = `
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<span>
<EuiLink
color="primary"
href="https://adamwdraper.github.io/Numeral-js/"
target="_window"
type="button"
>
Documentation
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
</span>
}
isInvalid={false}
label={
<span>
Numeral.js format pattern (Default:
<EuiCode>
0,0.[000]%
</EuiCode>
)
</span>
}
>
<EuiFieldText
compressed={false}
fullWidth={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
placeholder="0,0.[000]%"
/>
</EuiFormRow>
<FormatEditorSamples
samples={
Array [
Object {
"input": 0.1,
"output": 0.2,
},
Object {
"input": 0.99999,
"output": 1.99998,
},
Object {
"input": 1,
"output": 2,
},
Object {
"input": 100,
"output": 200,
},
Object {
"input": 1000,
"output": 2000,
},
]
}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { PercentFormatEditor } from './percent';

View file

@ -17,17 +17,17 @@
* under the License.
*/
import numeralTemplate from './numeral.html';
import { NumberFormatEditor } from '../number';
export function percentEditor() {
return {
formatId: 'percent',
template: numeralTemplate,
controllerAs: 'cntrl',
controller: function () {
this.sampleInputs = [
0.10, 0.99999, 1, 100, 1000
];
}
};
export class PercentFormatEditor extends NumberFormatEditor {
static formatId = 'percent';
constructor(props) {
super(props);
this.state = {
...this.state,
sampleInputs: [0.10, 0.99999, 1, 100, 1000],
};
}
}

View file

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { PercentFormatEditor } from './percent';
const fieldType = 'number';
const format = {
getConverterFor: jest.fn().mockImplementation(() => (input) => input * 2),
getParamDefaults: jest.fn().mockImplementation(() => {
return { pattern: '0,0.[000]%' };
}),
};
const formatParams = {};
const onChange = jest.fn();
const onError = jest.fn();
describe('PercentFormatEditor', () => {
it('should render normally', async () => {
const component = shallow(
<PercentFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,175 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StaticLookupFormatEditor should render multiple lookup entries and unknown key value 1`] = `
<React.Fragment>
<EuiBasicTable
columns={
Array [
Object {
"field": "key",
"name": "Key",
"render": [Function],
},
Object {
"field": "value",
"name": "Value",
"render": [Function],
},
Object {
"actions": Array [
Object {
"available": [Function],
"color": "danger",
"description": "Delete entry",
"icon": "trash",
"name": "Delete",
"onClick": [Function],
"type": "icon",
},
],
"width": "30px",
},
]
}
items={
Array [
Object {
"index": 0,
},
Object {
"index": 1,
},
Object {
"index": 2,
},
]
}
noItemsMessage="No items found"
responsive={true}
style={
Object {
"maxWidth": "400px",
}
}
/>
<EuiSpacer
size="m"
/>
<EuiButton
color="primary"
fill={false}
iconSide="left"
iconType="plusInCircle"
onClick={[Function]}
size="s"
type="button"
>
Add entry
</EuiButton>
<EuiSpacer
size="l"
/>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Value for unknown key"
>
<EuiFieldText
compressed={false}
fullWidth={false}
isLoading={false}
onChange={[Function]}
placeholder="Leave blank to keep value as-is"
value="test value"
/>
</EuiFormRow>
<EuiSpacer
size="m"
/>
</React.Fragment>
`;
exports[`StaticLookupFormatEditor should render normally 1`] = `
<React.Fragment>
<EuiBasicTable
columns={
Array [
Object {
"field": "key",
"name": "Key",
"render": [Function],
},
Object {
"field": "value",
"name": "Value",
"render": [Function],
},
Object {
"actions": Array [
Object {
"available": [Function],
"color": "danger",
"description": "Delete entry",
"icon": "trash",
"name": "Delete",
"onClick": [Function],
"type": "icon",
},
],
"width": "30px",
},
]
}
items={
Array [
Object {
"index": 0,
},
]
}
noItemsMessage="No items found"
responsive={true}
style={
Object {
"maxWidth": "400px",
}
}
/>
<EuiSpacer
size="m"
/>
<EuiButton
color="primary"
fill={false}
iconSide="left"
iconType="plusInCircle"
onClick={[Function]}
size="s"
type="button"
>
Add entry
</EuiButton>
<EuiSpacer
size="l"
/>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Value for unknown key"
>
<EuiFieldText
compressed={false}
fullWidth={false}
isLoading={false}
onChange={[Function]}
placeholder="Leave blank to keep value as-is"
value=""
/>
</EuiFormRow>
<EuiSpacer
size="m"
/>
</React.Fragment>
`;

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { StaticLookupFormatEditor } from './static_lookup';

View file

@ -0,0 +1,159 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import {
EuiBasicTable,
EuiButton,
EuiFieldText,
EuiFormRow,
EuiSpacer,
} from '@elastic/eui';
import {
DefaultFormatEditor
} from '../default';
export class StaticLookupFormatEditor extends DefaultFormatEditor {
static formatId = 'static_lookup';
onLookupChange = (newLookupParams, index) => {
const lookupEntries = [...this.props.formatParams.lookupEntries];
lookupEntries[index] = {
...lookupEntries[index],
...newLookupParams,
};
this.onChange({
lookupEntries,
});
}
addLookup = () => {
const lookupEntries = [...this.props.formatParams.lookupEntries];
this.onChange({
lookupEntries: [
...lookupEntries,
{}
],
});
}
removeLookup = (index) => {
const lookupEntries = [...this.props.formatParams.lookupEntries];
lookupEntries.splice(index, 1);
this.onChange({
lookupEntries,
});
}
render() {
const { formatParams } = this.props;
const items = formatParams.lookupEntries && formatParams.lookupEntries.length && formatParams.lookupEntries.map((lookup, index) => {
return {
...lookup,
index,
};
}) || [];
const columns = [
{
field: 'key',
name: 'Key',
render: (value, item) => {
return (
<EuiFieldText
value={value || ''}
onChange={(e) => {
this.onLookupChange({
key: e.target.value,
}, item.index);
}}
/>
);
}
},
{
field: 'value',
name: 'Value',
render: (value, item) => {
return (
<EuiFieldText
value={value || ''}
onChange={(e) => {
this.onLookupChange({
value: e.target.value,
}, item.index);
}}
/>
);
}
},
{
actions: [
{
name: 'Delete',
description: 'Delete entry',
onClick: (item) => {
this.removeLookup(item.index);
},
type: 'icon',
icon: 'trash',
color: 'danger',
available: () => items.length > 1,
}
],
width: '30px',
}
];
return (
<Fragment>
<EuiBasicTable
items={items}
columns={columns}
style={{ maxWidth: '400px' }}
/>
<EuiSpacer size="m" />
<EuiButton
iconType="plusInCircle"
size="s"
onClick={this.addLookup}
>
Add entry
</EuiButton>
<EuiSpacer size="l" />
<EuiFormRow
label="Value for unknown key"
>
<EuiFieldText
value={formatParams.unknownKeyValue || ''}
placeholder="Leave blank to keep value as-is"
onChange={(e) => {
this.onChange({ unknownKeyValue: e.target.value });
}}
/>
</EuiFormRow>
<EuiSpacer size="m" />
</Fragment>
);
}
}

View file

@ -0,0 +1,65 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { StaticLookupFormatEditor } from './static_lookup';
const fieldType = 'string';
const format = {
getConverterFor: jest.fn(),
};
const formatParams = {
lookupEntries: [{}],
unknownKeyValue: null,
};
const onChange = jest.fn();
const onError = jest.fn();
describe('StaticLookupFormatEditor', () => {
it('should render normally', async () => {
const component = shallow(
<StaticLookupFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
it('should render multiple lookup entries and unknown key value', async () => {
const component = shallow(
<StaticLookupFormatEditor
fieldType={fieldType}
format={format}
formatParams={{ lookupEntries: [{}, {}, {}], unknownKeyValue: 'test value' }}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StringFormatEditor should render normally 1`] = `
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Transform"
>
<EuiSelect
compressed={false}
data-test-subj="stringEditorTransform"
fullWidth={false}
hasNoInitialSelection={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"text": "Upper Case",
"value": "upper",
},
]
}
/>
</EuiFormRow>
<FormatEditorSamples
samples={
Array [
Object {
"input": "A Quick Brown Fox.",
"output": "A QUICK BROWN FOX.",
},
Object {
"input": "STAY CALM!",
"output": "STAY CALM!",
},
Object {
"input": "com.organizations.project.ClassName",
"output": "COM.ORGANIZATIONS.PROJECT.CLASSNAME",
},
Object {
"input": "hostname.net",
"output": "HOSTNAME.NET",
},
Object {
"input": "SGVsbG8gd29ybGQ=",
"output": "SGVSBG8GD29YBGQ=",
},
]
}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { StringFormatEditor } from './string';

View file

@ -0,0 +1,81 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import {
EuiFormRow,
EuiSelect,
} from '@elastic/eui';
import {
DefaultFormatEditor
} from '../default';
import {
FormatEditorSamples
} from '../../samples';
export class StringFormatEditor extends DefaultFormatEditor {
static formatId = 'string';
constructor(props) {
super(props);
this.state.sampleInputs = [
'A Quick Brown Fox.',
'STAY CALM!',
'com.organizations.project.ClassName',
'hostname.net',
'SGVsbG8gd29ybGQ='
];
}
render() {
const { format, formatParams } = this.props;
const { error, samples } = this.state;
return (
<Fragment>
<EuiFormRow
label="Transform"
isInvalid={!!error}
error={error}
>
<EuiSelect
data-test-subj="stringEditorTransform"
defaultValue={formatParams.transform}
options={format.type.transformOptions.map(option => {
return {
value: option.kind,
text: option.text,
};
})}
onChange={(e) => {
this.onChange({ transform: e.target.value });
}}
isInvalid={!!error}
/>
</EuiFormRow>
<FormatEditorSamples
samples={samples}
/>
</Fragment>
);
}
}

View file

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { StringFormatEditor } from './string';
const fieldType = 'string';
const format = {
getConverterFor: jest.fn().mockImplementation(() => (input) => input.toUpperCase()),
getParamDefaults: jest.fn().mockImplementation(() => {
return { transform: 'upper' };
}),
type: {
transformOptions: [
{
kind: 'upper',
text: 'Upper Case',
},
],
},
};
const formatParams = {};
const onChange = jest.fn();
const onError = jest.fn();
describe('StringFormatEditor', () => {
it('should render normally', async () => {
const component = shallow(
<StringFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TruncateFormatEditor should render normally 1`] = `
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Field length"
>
<EuiFieldNumber
compressed={false}
fullWidth={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
/>
</EuiFormRow>
<FormatEditorSamples
samples={
Array [
Object {
"input": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vitae sem consequat, sollicitudin enim a, feugiat mi. Curabitur congue laoreet elit, eu dictum nisi commodo ut. Nullam congue sem a blandit commodo. Suspendisse eleifend sodales leo ac hendrerit. Nam fringilla tempor fermentum. Ut tristique pharetra sapien sit amet pharetra. Ut turpis massa, viverra id erat quis, fringilla vehicula risus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus tincidunt gravida gravida. Praesent et ligula viverra, semper lacus in, tristique elit. Cras ac eleifend diam. Nulla facilisi. Morbi id sagittis magna. Sed fringilla, magna in suscipit aliquet.",
"output": "Lorem ipsu",
},
]
}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { TruncateFormatEditor } from './truncate';

View file

@ -0,0 +1 @@
export const sample = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vitae sem consequat, sollicitudin enim a, feugiat mi. Curabitur congue laoreet elit, eu dictum nisi commodo ut. Nullam congue sem a blandit commodo. Suspendisse eleifend sodales leo ac hendrerit. Nam fringilla tempor fermentum. Ut tristique pharetra sapien sit amet pharetra. Ut turpis massa, viverra id erat quis, fringilla vehicula risus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus tincidunt gravida gravida. Praesent et ligula viverra, semper lacus in, tristique elit. Cras ac eleifend diam. Nulla facilisi. Morbi id sagittis magna. Sed fringilla, magna in suscipit aliquet."; // eslint-disable-line

View file

@ -0,0 +1,72 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import {
EuiFieldNumber,
EuiFormRow,
} from '@elastic/eui';
import {
DefaultFormatEditor
} from '../default';
import {
FormatEditorSamples
} from '../../samples';
import {
sample
} from './sample';
export class TruncateFormatEditor extends DefaultFormatEditor {
static formatId = 'truncate';
constructor(props) {
super(props);
this.state.sampleInputs = [sample];
}
render() {
const { formatParams } = this.props;
const { error, samples } = this.state;
return (
<Fragment>
<EuiFormRow
label="Field length"
isInvalid={!!error}
error={error}
>
<EuiFieldNumber
defaultValue={formatParams.fieldLength}
onChange={(e) => {
this.onChange({ fieldLength: e.target.value ? Number(e.target.value) : null });
}}
isInvalid={!!error}
/>
</EuiFormRow>
<FormatEditorSamples
samples={samples}
/>
</Fragment>
);
}
}

View file

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { TruncateFormatEditor } from './truncate';
const fieldType = 'string';
const format = {
getConverterFor: jest.fn().mockImplementation(() => (input) => input.substring(0, 10)),
getParamDefaults: jest.fn().mockImplementation(() => {
return { fieldLength: 10 };
}),
};
const formatParams = {};
const onChange = jest.fn();
const onError = jest.fn();
describe('TruncateFormatEditor', () => {
it('should render normally', async () => {
const component = shallow(
<TruncateFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LabelTemplateFlyout should not render if not visible 1`] = `""`;
exports[`LabelTemplateFlyout should render normally 1`] = `
<EuiFlyout
hideCloseButton={false}
onClose={[Function]}
ownFocus={false}
size="m"
>
<EuiFlyoutBody>
<EuiText
grow={true}
>
<h3>
Label Template
</h3>
<p>
If the URL in this field is large, it might be useful to provide an alternate template for the text version of the URL. This will be displayed instead of the url, but will still link to the URL. The format is a string which uses double curly brace notation
<EuiCode>
{{ }}
</EuiCode>
to inject values. The following values can be accessed:
</p>
<ul>
<li>
<EuiCode>
value
</EuiCode>
— The fields value
</li>
<li>
<EuiCode>
url
</EuiCode>
— The formatted URL
</li>
</ul>
<h4>
Examples
</h4>
<EuiBasicTable
columns={
Array [
Object {
"field": "input",
"name": "Input",
"width": "160px",
},
Object {
"field": "urlTemplate",
"name": "URL Template",
},
Object {
"field": "labelTemplate",
"name": "Label Template",
},
Object {
"field": "output",
"name": "Output",
"render": [Function],
},
]
}
items={
Array [
Object {
"input": 1234,
"labelTemplate": "User #{{value}}",
"output": "<a href=\\"http://company.net/profiles?user_id=1234\\">User #1234</a>",
"urlTemplate": "http://company.net/profiles?user_id={{value}}",
},
Object {
"input": "/assets/main.css",
"labelTemplate": "View Asset",
"output": "<a href=\\"http://site.com/assets/main.css\\">View Asset</a>",
"urlTemplate": "http://site.com{{rawValue}}",
},
]
}
noItemsMessage="No items found"
responsive={true}
/>
</EuiText>
</EuiFlyoutBody>
</EuiFlyout>
`;

View file

@ -0,0 +1,298 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UrlFormatEditor should render label template help 1`] = `
<React.Fragment>
<LabelTemplateFlyout
isVisible={true}
onClose={[Function]}
/>
<UrlTemplateFlyout
isVisible={false}
onClose={[Function]}
/>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Type"
>
<EuiSelect
compressed={false}
data-test-subj="urlEditorType"
fullWidth={false}
hasNoInitialSelection={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"text": "Link",
"value": "a",
},
Object {
"text": "Image",
"value": "img",
},
Object {
"text": "Audio",
"value": "audio",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiLink
color="primary"
onClick={[Function]}
type="button"
>
URL template help
</EuiLink>
}
isInvalid={false}
label="URL template"
>
<EuiFieldText
compressed={false}
data-test-subj="urlEditorUrlTemplate"
fullWidth={false}
isLoading={false}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiLink
color="primary"
onClick={[Function]}
type="button"
>
Label template help
</EuiLink>
}
isInvalid={false}
label="Label template"
>
<EuiFieldText
compressed={false}
data-test-subj="urlEditorLabelTemplate"
fullWidth={false}
isLoading={false}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<FormatEditorSamples
samples={Array []}
/>
</React.Fragment>
`;
exports[`UrlFormatEditor should render normally 1`] = `
<React.Fragment>
<LabelTemplateFlyout
isVisible={false}
onClose={[Function]}
/>
<UrlTemplateFlyout
isVisible={false}
onClose={[Function]}
/>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Type"
>
<EuiSelect
compressed={false}
data-test-subj="urlEditorType"
fullWidth={false}
hasNoInitialSelection={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"text": "Link",
"value": "a",
},
Object {
"text": "Image",
"value": "img",
},
Object {
"text": "Audio",
"value": "audio",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiLink
color="primary"
onClick={[Function]}
type="button"
>
URL template help
</EuiLink>
}
isInvalid={false}
label="URL template"
>
<EuiFieldText
compressed={false}
data-test-subj="urlEditorUrlTemplate"
fullWidth={false}
isLoading={false}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiLink
color="primary"
onClick={[Function]}
type="button"
>
Label template help
</EuiLink>
}
isInvalid={false}
label="Label template"
>
<EuiFieldText
compressed={false}
data-test-subj="urlEditorLabelTemplate"
fullWidth={false}
isLoading={false}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<FormatEditorSamples
samples={Array []}
/>
</React.Fragment>
`;
exports[`UrlFormatEditor should render url template help 1`] = `
<React.Fragment>
<LabelTemplateFlyout
isVisible={false}
onClose={[Function]}
/>
<UrlTemplateFlyout
isVisible={true}
onClose={[Function]}
/>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Type"
>
<EuiSelect
compressed={false}
data-test-subj="urlEditorType"
fullWidth={false}
hasNoInitialSelection={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"text": "Link",
"value": "a",
},
Object {
"text": "Image",
"value": "img",
},
Object {
"text": "Audio",
"value": "audio",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiLink
color="primary"
onClick={[Function]}
type="button"
>
URL template help
</EuiLink>
}
isInvalid={false}
label="URL template"
>
<EuiFieldText
compressed={false}
data-test-subj="urlEditorUrlTemplate"
fullWidth={false}
isLoading={false}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiLink
color="primary"
onClick={[Function]}
type="button"
>
Label template help
</EuiLink>
}
isInvalid={false}
label="Label template"
>
<EuiFieldText
compressed={false}
data-test-subj="urlEditorLabelTemplate"
fullWidth={false}
isLoading={false}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<FormatEditorSamples
samples={Array []}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,90 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UrlTemplateFlyout should not render if not visible 1`] = `""`;
exports[`UrlTemplateFlyout should render normally 1`] = `
<EuiFlyout
hideCloseButton={false}
onClose={[Function]}
ownFocus={false}
size="m"
>
<EuiFlyoutBody>
<EuiText
grow={true}
>
<h3>
Url Template
</h3>
<p>
If a field only contains part of a URL then a
<strong>
Url Template
</strong>
can be used to format the value as a complete URL. The format is a string which uses double curly brace notation
<EuiCode>
{{ }}
</EuiCode>
to inject values. The following values can be accessed:
</p>
<ul>
<li>
<EuiCode>
value
</EuiCode>
— The URI-escaped value
</li>
<li>
<EuiCode>
rawValue
</EuiCode>
— The unescaped value
</li>
</ul>
<h4>
Examples
</h4>
<EuiBasicTable
columns={
Array [
Object {
"field": "input",
"name": "Input",
"width": "160px",
},
Object {
"field": "template",
"name": "Template",
},
Object {
"field": "output",
"name": "Output",
},
]
}
items={
Array [
Object {
"input": 1234,
"output": "http://company.net/profiles?user_id=1234",
"template": "http://company.net/profiles?user_id={{value}}",
},
Object {
"input": "users/admin",
"output": "http://company.net/groups?id=users%2Fadmin",
"template": "http://company.net/groups?id={{value}",
},
Object {
"input": "/images/favicon.ico",
"output": "http://www.site.com/images/favicon.ico",
"template": "http://www.site.com{{rawValue}}",
},
]
}
noItemsMessage="No items found"
responsive={true}
/>
</EuiText>
</EuiFlyoutBody>
</EuiFlyout>
`;

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import '!!file-loader?name=[path][name].[ext]!ui/field_editor/components/field_format_editor/editors/url/icons/go.png';
import '!!file-loader?name=[path][name].[ext]!ui/field_editor/components/field_format_editor/editors/url/icons/stop.png';
import '!!file-loader?name=[path][name].[ext]!ui/field_editor/components/field_format_editor/editors/url/icons/de.png';
import '!!file-loader?name=[path][name].[ext]!ui/field_editor/components/field_format_editor/editors/url/icons/ne.png';
import '!!file-loader?name=[path][name].[ext]!ui/field_editor/components/field_format_editor/editors/url/icons/us.png';
import '!!file-loader?name=[path][name].[ext]!ui/field_editor/components/field_format_editor/editors/url/icons/ni.png';
import '!!file-loader?name=[path][name].[ext]!ui/field_editor/components/field_format_editor/editors/url/icons/cv.png';

View file

@ -17,4 +17,4 @@
* under the License.
*/
import './scripted_field_editor';
export { UrlFormatEditor } from './url';

View file

@ -0,0 +1,108 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import {
EuiBasicTable,
EuiCode,
EuiFlyout,
EuiFlyoutBody,
EuiText,
} from '@elastic/eui';
export const LabelTemplateFlyout = ({
isVisible = false,
onClose = () => {},
}) => {
return isVisible ? (
<EuiFlyout
onClose={onClose}
>
<EuiFlyoutBody>
<EuiText>
<h3>Label Template</h3>
<p>
If the URL in this field is large, it might be useful to provide an alternate template for the text
version of the URL. This will be displayed instead of the url, but will still link to the URL. The
format is a string which uses double curly brace notation <EuiCode>{('{{ }}')}</EuiCode>
to inject values. The following values can be accessed:
</p>
<ul>
<li>
<EuiCode>value</EuiCode> &mdash; The fields value
</li>
<li>
<EuiCode>url</EuiCode> &mdash; The formatted URL
</li>
</ul>
<h4>Examples</h4>
<EuiBasicTable
items={[
{
input: 1234,
urlTemplate: 'http://company.net/profiles?user_id={{value}}',
labelTemplate: 'User #{{value}}',
output: '<a href="http://company.net/profiles?user_id=1234">User #1234</a>',
},
{
input: '/assets/main.css',
urlTemplate: 'http://site.com{{rawValue}}',
labelTemplate: 'View Asset',
output: '<a href="http://site.com/assets/main.css">View Asset</a>',
},
]}
columns={[
{
field: 'input',
name: 'Input',
width: '160px',
},
{
field: 'urlTemplate',
name: 'URL Template',
},
{
field: 'labelTemplate',
name: 'Label Template',
},
{
field: 'output',
name: 'Output',
render: (value) => {
return (
<span
/*
* Justification for dangerouslySetInnerHTML:
* Example output produces anchor link.
*/
dangerouslySetInnerHTML={{ __html: value }} //eslint-disable-line react/no-danger
/>
);
}
},
]}
/>
</EuiText>
</EuiFlyoutBody>
</EuiFlyout>
) : null;
};
LabelTemplateFlyout.displayName = 'LabelTemplateFlyout';

View file

@ -0,0 +1,41 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { LabelTemplateFlyout } from './label_template_flyout';
describe('LabelTemplateFlyout', () => {
it('should render normally', async () => {
const component = shallow(
<LabelTemplateFlyout
isVisible={true}
/>
);
expect(component).toMatchSnapshot();
});
it('should not render if not visible', async () => {
const component = shallow(
<LabelTemplateFlyout />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,192 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import {
EuiFieldText,
EuiFormRow,
EuiLink,
EuiSelect,
EuiSwitch,
} from '@elastic/eui';
import {
DefaultFormatEditor
} from '../default';
import {
FormatEditorSamples
} from '../../samples';
import {
LabelTemplateFlyout
} from './label_template_flyout';
import {
UrlTemplateFlyout
} from './url_template_flyout';
import chrome from 'ui/chrome';
import './icons';
export class UrlFormatEditor extends DefaultFormatEditor {
static formatId = 'url';
constructor(props) {
super(props);
const bp = chrome.getBasePath();
this.iconPattern = `${bp}/bundles/src/ui/public/field_editor/components/field_format_editor/editors/url/icons/{{value}}.png`;
this.state = {
...this.state,
sampleInputsByType: {
a: [ 'john', '/some/pathname/asset.png', 1234 ],
img: [ 'go', 'stop', ['de', 'ne', 'us', 'ni'], 'cv' ],
audio: [ 'hello.mp3' ],
},
sampleConverterType: 'html',
showUrlTemplateHelp: false,
showLabelTemplateHelp: false,
};
}
onTypeChange = (newType) => {
const { urlTemplate } = this.props.formatParams;
if(newType === 'img' && !urlTemplate) {
this.onChange({
type: newType,
urlTemplate: this.iconPattern,
});
} else if(newType !== 'img' && urlTemplate === this.iconPattern) {
this.onChange({
type: newType,
urlTemplate: null,
});
} else {
this.onChange({
type: newType,
});
}
}
showUrlTemplateHelp = () => {
this.setState({
showLabelTemplateHelp: false,
showUrlTemplateHelp: true,
});
}
hideUrlTemplateHelp = () => {
this.setState({
showUrlTemplateHelp: false,
});
}
showLabelTemplateHelp = () => {
this.setState({
showLabelTemplateHelp: true,
showUrlTemplateHelp: false,
});
}
hideLabelTemplateHelp = () => {
this.setState({
showLabelTemplateHelp: false,
});
}
render() {
const { format, formatParams } = this.props;
const { error, samples } = this.state;
return (
<Fragment>
<LabelTemplateFlyout
isVisible={this.state.showLabelTemplateHelp}
onClose={this.hideLabelTemplateHelp}
/>
<UrlTemplateFlyout
isVisible={this.state.showUrlTemplateHelp}
onClose={this.hideUrlTemplateHelp}
/>
<EuiFormRow label="Type">
<EuiSelect
data-test-subj="urlEditorType"
value={formatParams.type}
options={format.type.urlTypes.map(type => {
return {
value: type.kind,
text: type.text,
};
})}
onChange={(e) => {
this.onTypeChange(e.target.value);
}}
/>
</EuiFormRow>
{formatParams.type === 'a' ? (
<EuiFormRow label="Open in a new tab">
<EuiSwitch
label={formatParams.openLinkInCurrentTab ? 'Off' : 'On'}
checked={!formatParams.openLinkInCurrentTab}
onChange={(e) => {
this.onChange({ openLinkInCurrentTab: !e.target.checked });
}}
/>
</EuiFormRow>
) : null}
<EuiFormRow
label="URL template"
helpText={(<EuiLink onClick={this.showUrlTemplateHelp}>URL template help</EuiLink>)}
isInvalid={!!error}
error={error}
>
<EuiFieldText
data-test-subj="urlEditorUrlTemplate"
value={formatParams.urlTemplate || ''}
onChange={(e) => {
this.onChange({ urlTemplate: e.target.value });
}}
/>
</EuiFormRow>
<EuiFormRow
label="Label template"
helpText={(<EuiLink onClick={this.showLabelTemplateHelp}>Label template help</EuiLink>)}
isInvalid={!!error}
error={error}
>
<EuiFieldText
data-test-subj="urlEditorLabelTemplate"
value={formatParams.labelTemplate || ''}
onChange={(e) => {
this.onChange({ labelTemplate: e.target.value });
}}
/>
</EuiFormRow>
<FormatEditorSamples
samples={samples}
/>
</Fragment>
);
}
}

View file

@ -0,0 +1,91 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { UrlFormatEditor } from './url';
const fieldType = 'string';
const format = {
getConverterFor: jest.fn().mockImplementation(() => (input) => `converted url for ${input}`),
type: {
urlTypes: [
{ kind: 'a', text: 'Link' },
{ kind: 'img', text: 'Image' },
{ kind: 'audio', text: 'Audio' }
],
},
};
const formatParams = {};
const onChange = jest.fn();
const onError = jest.fn();
jest.mock('ui/chrome', () => ({
getBasePath: () => 'http://localhost/',
}));
describe('UrlFormatEditor', () => {
it('should render normally', async () => {
const component = shallow(
<UrlFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
expect(component).toMatchSnapshot();
});
it('should render url template help', async () => {
const component = shallow(
<UrlFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
component.instance().showUrlTemplateHelp();
component.update();
expect(component).toMatchSnapshot();
});
it('should render label template help', async () => {
const component = shallow(
<UrlFormatEditor
fieldType={fieldType}
format={format}
formatParams={formatParams}
onChange={onChange}
onError={onError}
/>
);
component.instance().showLabelTemplateHelp();
component.update();
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,95 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import {
EuiBasicTable,
EuiCode,
EuiFlyout,
EuiFlyoutBody,
EuiText,
} from '@elastic/eui';
export const UrlTemplateFlyout = ({
isVisible = false,
onClose = () => {},
}) => {
return isVisible ? (
<EuiFlyout
onClose={onClose}
>
<EuiFlyoutBody>
<EuiText>
<h3>Url Template</h3>
<p>
If a field only contains part of a URL then a <strong>Url Template</strong> can be used to format the value
as a complete URL. The format is a string which uses double curly brace notation <EuiCode>{('{{ }}')}</EuiCode>
to inject values. The following values can be accessed:
</p>
<ul>
<li>
<EuiCode>value</EuiCode> &mdash; The URI-escaped value
</li>
<li>
<EuiCode>rawValue</EuiCode> &mdash; The unescaped value
</li>
</ul>
<h4>Examples</h4>
<EuiBasicTable
items={[
{
input: 1234,
template: 'http://company.net/profiles?user_id={{value}}',
output: 'http://company.net/profiles?user_id=1234',
},
{
input: 'users/admin',
template: 'http://company.net/groups?id={{value}',
output: 'http://company.net/groups?id=users%2Fadmin',
},
{
input: '/images/favicon.ico',
template: 'http://www.site.com{{rawValue}}',
output: 'http://www.site.com/images/favicon.ico',
},
]}
columns={[
{
field: 'input',
name: 'Input',
width: '160px',
},
{
field: 'template',
name: 'Template',
},
{
field: 'output',
name: 'Output',
},
]}
/>
</EuiText>
</EuiFlyoutBody>
</EuiFlyout>
) : null;
};
UrlTemplateFlyout.displayName = 'UrlTemplateFlyout';

View file

@ -17,26 +17,25 @@
* under the License.
*/
import './duration.less';
import durationTemplate from './duration.html';
import React from 'react';
import { shallow } from 'enzyme';
export function durationEditor() {
return {
formatId: 'duration',
template: durationTemplate,
controllerAs: 'cntrl',
controller() {
this.sampleInputs = [
-123,
1,
12,
123,
658,
1988,
3857,
123292,
923528271
];
}
};
}
import { UrlTemplateFlyout } from './url_template_flyout';
describe('UrlTemplateFlyout', () => {
it('should render normally', async () => {
const component = shallow(
<UrlTemplateFlyout
isVisible={true}
/>
);
expect(component).toMatchSnapshot();
});
it('should not render if not visible', async () => {
const component = shallow(
<UrlTemplateFlyout />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,65 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
export class FieldFormatEditor extends PureComponent {
static propTypes = {
fieldType: PropTypes.string.isRequired,
fieldFormat: PropTypes.object.isRequired,
fieldFormatId: PropTypes.string.isRequired,
fieldFormatParams: PropTypes.object.isRequired,
fieldFormatEditors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onError: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
EditorComponent: null,
};
}
static getDerivedStateFromProps(nextProps) {
return {
EditorComponent: nextProps.fieldFormatEditors.getEditor(nextProps.fieldFormatId) || null,
};
}
render() {
const { EditorComponent } = this.state;
const { fieldType, fieldFormat, fieldFormatParams, onChange, onError } = this.props;
return (
<Fragment>
{ EditorComponent ? (
<EditorComponent
fieldType={fieldType}
format={fieldFormat}
formatParams={fieldFormatParams}
onChange={onChange}
onError={onError}
/>
) : null}
</Fragment>
);
}
}

View file

@ -0,0 +1,74 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { PureComponent } from 'react';
import { shallow } from 'enzyme';
import { FieldFormatEditor } from './field_format_editor';
class TestEditor extends PureComponent {
render() {
if(this.props) {
return null;
}
return <div>Test editor</div>;
}
}
describe('FieldFormatEditor', () => {
it('should render normally', async () => {
const component = shallow(
<FieldFormatEditor
fieldType="number"
fieldFormat={{}}
fieldFormatId="number"
fieldFormatParams={{}}
fieldFormatEditors={{
getEditor: () => {
return TestEditor;
}
}}
onChange={() => {}}
onError={() => {}}
/>
);
expect(component).toMatchSnapshot();
});
it('should render nothing if there is no editor for the format', async () => {
const component = shallow(
<FieldFormatEditor
fieldType="number"
fieldFormat={{}}
fieldFormatId="ip"
fieldFormatParams={{}}
fieldFormatEditors={{
getEditor: () => {
return null;
}
}}
onChange={() => {}}
onError={() => {}}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { FieldFormatEditor } from './field_format_editor';

View file

@ -0,0 +1,41 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors';
import { BytesFormatEditor } from './editors/bytes';
import { ColorFormatEditor } from './editors/color';
import { DateFormatEditor } from './editors/date';
import { DurationFormatEditor } from './editors/duration';
import { NumberFormatEditor } from './editors/number';
import { PercentFormatEditor } from './editors/percent';
import { StaticLookupFormatEditor } from './editors/static_lookup';
import { StringFormatEditor } from './editors/string';
import { TruncateFormatEditor } from './editors/truncate';
import { UrlFormatEditor } from './editors/url/url';
RegistryFieldFormatEditorsProvider.register(() => BytesFormatEditor);
RegistryFieldFormatEditorsProvider.register(() => ColorFormatEditor);
RegistryFieldFormatEditorsProvider.register(() => DateFormatEditor);
RegistryFieldFormatEditorsProvider.register(() => DurationFormatEditor);
RegistryFieldFormatEditorsProvider.register(() => NumberFormatEditor);
RegistryFieldFormatEditorsProvider.register(() => PercentFormatEditor);
RegistryFieldFormatEditorsProvider.register(() => StaticLookupFormatEditor);
RegistryFieldFormatEditorsProvider.register(() => StringFormatEditor);
RegistryFieldFormatEditorsProvider.register(() => TruncateFormatEditor);
RegistryFieldFormatEditorsProvider.register(() => UrlFormatEditor);

View file

@ -0,0 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormatEditorSamples should render normally 1`] = `
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Samples"
>
<EuiBasicTable
className="fieldFormatEditor__samples"
columns={
Array [
Object {
"field": "input",
"name": "Input",
"render": [Function],
},
Object {
"field": "output",
"name": "Output",
"render": [Function],
},
]
}
compressed={true}
items={
Array [
Object {
"input": "test",
"output": "TEST",
},
Object {
"input": 123,
"output": 456,
},
Object {
"input": Array [
"foo",
"bar",
],
"output": "<span>foo</span>, <span>bar</span>",
},
]
}
noItemsMessage="No items found"
responsive={true}
/>
</EuiFormRow>
`;
exports[`FormatEditorSamples should render nothing if there are no samples 1`] = `""`;

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { FormatEditorSamples } from './samples';

View file

@ -0,0 +1,80 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
EuiBasicTable,
EuiFormRow,
} from '@elastic/eui';
import './samples.less';
export class FormatEditorSamples extends PureComponent {
static propTypes = {
samples: PropTypes.arrayOf(PropTypes.shape({
input: PropTypes.any.isRequired,
output: PropTypes.any.isRequired,
})).isRequired,
};
render() {
const { samples } = this.props;
const columns = [
{
field: 'input',
name: 'Input',
render: (input) => {
return typeof input === 'object' ? JSON.stringify(input) : input;
},
},
{
field: 'output',
name: 'Output',
render: (output) => {
return (
<div
/*
* Justification for dangerouslySetInnerHTML:
* Sample output may contain HTML tags, like URL image/audio format.
*/
dangerouslySetInnerHTML={{ __html: output }} //eslint-disable-line react/no-danger
/>
);
},
}
];
return samples.length ? (
<EuiFormRow
label="Samples"
>
<EuiBasicTable
className="fieldFormatEditor__samples"
compressed={true}
items={samples}
columns={columns}
/>
</EuiFormRow>
) : null;
}
}

View file

@ -0,0 +1,5 @@
.fieldFormatEditor__samples {
audio {
max-width: 100%;
}
}

View file

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { FormatEditorSamples } from './samples';
describe('FormatEditorSamples', () => {
it('should render normally', async () => {
const component = shallow(
<FormatEditorSamples
samples={[
{ input: 'test', output: 'TEST' },
{ input: 123, output: 456 },
{ input: ['foo', 'bar'], output: '<span>foo</span>, <span>bar</span>' },
]}
/>
);
expect(component).toMatchSnapshot();
});
it('should render nothing if there are no samples', async () => {
const component = shallow(
<FormatEditorSamples
samples={[]}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScriptingDisabledCallOut should render normally 1`] = `
<React.Fragment>
<EuiCallOut
color="danger"
iconType="alert"
size="m"
title="Scripting disabled"
>
<p>
All inline scripting has been disabled in Elasticsearch. You must enable inline scripting for at least one language in order to use scripted fields in Kibana.
</p>
</EuiCallOut>
<EuiSpacer
size="m"
/>
</React.Fragment>
`;
exports[`ScriptingDisabledCallOut should render nothing if not visible 1`] = `""`;

View file

@ -0,0 +1,134 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScriptingHelpFlyout should render normally 1`] = `
<EuiFlyout
hideCloseButton={false}
onClose={[Function]}
ownFocus={false}
size="s"
>
<EuiFlyoutBody>
<EuiText
grow={true}
>
<h3>
Scripting help
</h3>
<p>
By default, Kibana scripted fields use
<EuiLink
color="primary"
href="(docLink for scriptedFields.painless)"
target="_window"
type="button"
>
Painless
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
, a simple and secure scripting language designed specifically for use with Elasticsearch, to access values in the document use the following format:
</p>
<p>
<EuiCode>
doc['some_field'].value
</EuiCode>
</p>
<p>
Painless is powerful but easy to use. It provides access to many
<EuiLink
color="primary"
href="(docLink for scriptedFields.painlessApi)"
target="_window"
type="button"
>
native Java APIs
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
. Read up on its
<EuiLink
color="primary"
href="(docLink for scriptedFields.painlessSyntax)"
target="_window"
type="button"
>
syntax
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
and you'll be up to speed in no time!
</p>
<p>
Kibana currently imposes one special limitation on the painless scripts you write. They cannot contain named functions.
</p>
<p>
Coming from an older version of Kibana? The
<EuiLink
color="primary"
href="(docLink for scriptedFields.luceneExpressions)"
target="_window"
type="button"
>
Lucene Expressions
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
you know and love are still available. Lucene expressions are a lot like JavaScript, but limited to basic arithmetic, bitwise and comparison operations.
</p>
<p>
There are a few limitations when using Lucene Expressions:
</p>
<ul>
<li>
Only numeric, boolean, date, and geo_point fields may be accessed
</li>
<li>
Stored fields are not available
</li>
<li>
If a field is sparse (only some documents contain a value), documents missing the field will have a value of 0
</li>
</ul>
<p>
Here are all the operations available to lucene expressions:
</p>
<ul>
<li>
Arithmetic operators: + - * / %
</li>
<li>
Bitwise operators: | & ^ ~ &lt;&lt; &gt;&gt; &gt;&gt;&gt;
</li>
<li>
Boolean operators (including the ternary operator): && || ! ?:
</li>
<li>
Comparison operators: &lt; &lt;= == &gt;= &gt;
</li>
<li>
Common mathematic functions: abs ceil exp floor ln log10 logn max min sqrt pow
</li>
<li>
Trigonometric library functions: acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan
</li>
<li>
Distance functions: haversin
</li>
<li>
Miscellaneous functions: min, max
</li>
</ul>
</EuiText>
</EuiFlyoutBody>
</EuiFlyout>
`;
exports[`ScriptingHelpFlyout should render nothing if not visible 1`] = `""`;

View file

@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScriptingWarningCallOut should render normally 1`] = `
<React.Fragment>
<EuiCallOut
color="warning"
iconType="alert"
size="m"
title="Proceed with caution"
>
<p>
Please familiarize yourself with
<EuiLink
color="primary"
href="(docLink for scriptedFields.scriptFields)"
target="_window"
type="button"
>
script fields
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
and with
<EuiLink
color="primary"
href="(docLink for scriptedFields.scriptAggs)"
target="_window"
type="button"
>
scripts in aggregations
<EuiIcon
size="m"
type="link"
/>
</EuiLink>
before using scripted fields.
</p>
<p>
Scripted fields can be used to display and aggregate calculated values. As such, they can be very slow, and if done incorrectly, can cause Kibana to be unusable. There's no safety net here. If you make a typo, unexpected exceptions will be thrown all over the place!
</p>
</EuiCallOut>
<EuiSpacer
size="m"
/>
</React.Fragment>
`;
exports[`ScriptingWarningCallOut should render nothing if not visible 1`] = `""`;

View file

@ -17,24 +17,31 @@
* under the License.
*/
import './static_lookup.less';
import staticLookupTemplate from './static_lookup.html';
import React, { Fragment } from 'react';
export function staticLookupEditor() {
return {
formatId: 'static_lookup',
template: staticLookupTemplate,
controllerAs: 'staticLookupController',
controller: class StaticLookupController {
constructor($scope) {
this.formatParams = $scope.editor.formatParams;
if (!Array.isArray(this.formatParams.lookupEntries)) {
this.formatParams.lookupEntries = [];
}
}
import {
EuiCallOut,
EuiSpacer,
} from '@elastic/eui';
addEntry = () => this.formatParams.lookupEntries.push({});
removeEntry = (index) => this.formatParams.lookupEntries.splice(index, 1);
}
};
}
export const ScriptingDisabledCallOut = ({
isVisible = false,
}) => {
return isVisible ? (
<Fragment>
<EuiCallOut
title="Scripting disabled"
color="danger"
iconType="alert"
>
<p>
All inline scripting has been disabled in Elasticsearch. You must enable inline
scripting for at least one language in order to use scripted fields in Kibana.
</p>
</EuiCallOut>
<EuiSpacer size="m" />
</Fragment>
) : null;
};
ScriptingDisabledCallOut.displayName = 'ScriptingDisabledCallOut';

View file

@ -0,0 +1,43 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { ScriptingDisabledCallOut } from './disabled_call_out';
describe('ScriptingDisabledCallOut', () => {
it('should render normally', async () => {
const component = shallow(
<ScriptingDisabledCallOut
isVisible={true}
/>
);
expect(component).toMatchSnapshot();
});
it('should render nothing if not visible', async () => {
const component = shallow(
<ScriptingDisabledCallOut />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,113 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { getDocLink } from 'ui/documentation_links';
import {
EuiCode,
EuiFlyout,
EuiFlyoutBody,
EuiIcon,
EuiLink,
EuiText,
} from '@elastic/eui';
export const ScriptingHelpFlyout = ({
isVisible = false,
onClose = () => {},
}) => {
return isVisible ? (
<EuiFlyout onClose={onClose} size="s">
<EuiFlyoutBody>
<EuiText>
<h3>Scripting help</h3>
<p>
By default, Kibana scripted fields use {(
<EuiLink
target="_window"
href={getDocLink('scriptedFields.painless')}
>
Painless <EuiIcon type="link"/>
</EuiLink>
)}, a simple and secure scripting language designed specifically for use with Elasticsearch,
to access values in the document use the following format:
</p>
<p>
<EuiCode>doc[&apos;some_field&apos;].value</EuiCode>
</p>
<p>
Painless is powerful but easy to use. It provides access to many {(
<EuiLink
target="_window"
href={getDocLink('scriptedFields.painlessApi')}
>
native Java APIs <EuiIcon type="link"/>
</EuiLink>
)}. Read up on its {(
<EuiLink
target="_window"
href={getDocLink('scriptedFields.painlessSyntax')}
>
syntax <EuiIcon type="link"/>
</EuiLink>
)} and you&apos;ll be up to speed in no time!
</p>
<p>
Kibana currently imposes one special limitation on the painless scripts you write. They cannot contain named functions.
</p>
<p>
Coming from an older version of Kibana? The {(
<EuiLink
target="_window"
href={getDocLink('scriptedFields.luceneExpressions')}
>
Lucene Expressions <EuiIcon type="link"/>
</EuiLink>
)} you know and love are still available. Lucene expressions are a lot like JavaScript,
but limited to basic arithmetic, bitwise and comparison operations.
</p>
<p>
There are a few limitations when using Lucene Expressions:
</p>
<ul>
<li> Only numeric, boolean, date, and geo_point fields may be accessed</li>
<li> Stored fields are not available</li>
<li> If a field is sparse (only some documents contain a value), documents missing the field will have a value of 0</li>
</ul>
<p>
Here are all the operations available to lucene expressions:
</p>
<ul>
<li> Arithmetic operators: + - * / %</li>
<li> Bitwise operators: | & ^ ~ &#x3C;&#x3C; &#x3E;&#x3E; &#x3E;&#x3E;&#x3E;</li>
<li> Boolean operators (including the ternary operator): && || ! ?:</li>
<li> Comparison operators: &#x3C; &#x3C;= == &#x3E;= &#x3E;</li>
<li> Common mathematic functions: abs ceil exp floor ln log10 logn max min sqrt pow</li>
<li> Trigonometric library functions: acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan</li>
<li> Distance functions: haversin</li>
<li> Miscellaneous functions: min, max</li>
</ul>
</EuiText>
</EuiFlyoutBody>
</EuiFlyout>
) : null;
};
ScriptingHelpFlyout.displayName = 'ScriptingHelpFlyout';

View file

@ -17,27 +17,31 @@
* under the License.
*/
import dateTemplate from './date.html';
import moment from 'moment';
import React from 'react';
import { shallow } from 'enzyme';
export function dateEditor() {
return {
formatId: 'date',
template: dateTemplate,
controllerAs: 'cntrl',
controller: function ($interval, $scope) {
this.sampleInputs = [
Date.now(),
moment().startOf('year').valueOf(),
moment().endOf('year').valueOf()
];
import { ScriptingHelpFlyout } from './help_flyout';
const stop = $interval(() => {
this.sampleInputs[0] = Date.now();
}, 1000);
$scope.$on('$destroy', () => {
$interval.cancel(stop);
});
}
};
}
jest.mock('ui/documentation_links', () => ({
getDocLink: (doc) => `(docLink for ${doc})`,
}));
describe('ScriptingHelpFlyout', () => {
it('should render normally', async () => {
const component = shallow(
<ScriptingHelpFlyout
isVisible={true}
/>
);
expect(component).toMatchSnapshot();
});
it('should render nothing if not visible', async () => {
const component = shallow(
<ScriptingHelpFlyout />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { ScriptingDisabledCallOut } from './disabled_call_out';
export { ScriptingWarningCallOut } from './warning_call_out';
export { ScriptingHelpFlyout } from './help_flyout';

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import { getDocLink } from 'ui/documentation_links';
import {
EuiCallOut,
EuiIcon,
EuiLink,
EuiSpacer,
} from '@elastic/eui';
export const ScriptingWarningCallOut = ({
isVisible = false,
}) => {
return isVisible ? (
<Fragment>
<EuiCallOut
title="Proceed with caution"
color="warning"
iconType="alert"
>
<p>
Please familiarize yourself with {(
<EuiLink
target="_window"
href={getDocLink('scriptedFields.scriptFields')}
>
script fields <EuiIcon type="link"/>
</EuiLink>
)} and with {(
<EuiLink
target="_window"
href={getDocLink('scriptedFields.scriptAggs')}
>
scripts in aggregations <EuiIcon type="link"/>
</EuiLink>
)} before using scripted fields.
</p>
<p>
Scripted fields can be used to display and aggregate calculated values. As such,
they can be very slow, and if done incorrectly, can cause Kibana to be unusable.
There&apos;s no safety net here. If you make a typo, unexpected exceptions will
be thrown all over the place!
</p>
</EuiCallOut>
<EuiSpacer size="m" />
</Fragment>
) : null;
};
ScriptingWarningCallOut.displayName = 'ScriptingWarningCallOut';

View file

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { ScriptingWarningCallOut } from './warning_call_out';
jest.mock('ui/documentation_links', () => ({
getDocLink: (doc) => `(docLink for ${doc})`,
}));
describe('ScriptingWarningCallOut', () => {
it('should render normally', async () => {
const component = shallow(
<ScriptingWarningCallOut
isVisible={true}
/>
);
expect(component).toMatchSnapshot();
});
it('should render nothing if not visible', async () => {
const component = shallow(
<ScriptingWarningCallOut />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getKbnTypeNames } from '../../../../utils';
export const FIELD_TYPES_BY_LANG = {
painless: ['number', 'string', 'date', 'boolean'],
expression: ['number'],
};
export const DEFAULT_FIELD_TYPES = getKbnTypeNames();

View file

@ -1,288 +0,0 @@
<form ng-submit="editor.save()" name="form">
<div ng-if="editor.scriptingLangs.length === 0" class="hintbox">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>Scripting disabled:</strong>
All inline scripting has been disabled in Elasticsearch. You must enable inline scripting for at least one language in order to use scripted fields in Kibana.
</p>
</div>
<div ng-if="editor.creating" class="form-group">
<label for="scriptedFieldName" class="kuiFormLabel">Name</label>
<input
ng-model="editor.field.name"
id="scriptedFieldName"
required
placeholder="New Scripted Field"
input-focus
class="form-control"
data-test-subj="editorFieldName">
</div>
<div ng-if="editor.creating && editor.existingFieldNames.includes(editor.field.name)" class="hintbox">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>Mapping Conflict:</strong>
You already have a field with the name {{ editor.field.name }}. Naming your scripted
field with the same name means you won't be able to query both fields at the same time.
</p>
</div>
<div ng-if="editor.field.scripted" class="form-group">
<label for="scriptedFieldLang" class="kuiFormLabel">Language</label>
<div class="kuiInfoPanel kuiInfoPanel--warning kuiVerticalRhythm" ng-if="editor.field.lang && editor.isDeprecatedLang(editor.field.lang)">
<div class="kuiInfoPanelHeader">
<span
class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--warning fa-bolt"
aria-label="Warning"
role="img"
></span>
<span class="kuiInfoPanelHeader__title">
Deprecation Warning
</span>
</div>
<div class="kuiInfoPanelBody">
<div class="kuiInfoPanelBody__message">
<span class="text-capitalize">{{editor.field.lang}}</span> is deprecated and support will be removed in the
next major version of Kibana and Elasticsearch. We recommend using
<a class="kuiLink" ng-href="{{editor.docLinks.painless}}">Painless</a>
for new scripted fields.
</div>
</div>
</div>
<select
ng-model="editor.field.lang"
id="scriptedFieldLang"
ng-options="lang as lang for lang in editor.scriptingLangs"
required
class="form-control kuiVerticalRhythm"
data-test-subj="editorFieldLang">
<option value="">-- Select Language --</option>
</select>
</div>
<div class="form-group">
<label for="scriptedFieldType" class="kuiFormLabel">Type</label>
<select
ng-if="editor.field.scripted"
id="scriptedFieldType"
ng-model="editor.field.type"
ng-options="type as type for type in editor.fieldTypes"
class="form-control"
data-test-subj="editorFieldType">
</select>
<input
ng-if="!editor.field.scripted"
id="scriptedFieldType"
ng-model="editor.field.type"
readonly
class="form-control">
</div>
<div class="form-group">
<span class="pull-right text-warning hintbox-label" ng-click="editor.showFormatHelp = !editor.showFormatHelp">
<i class="fa fa-warning"></i> Warning
</span>
<label for="scriptFieldFormat" class="kuiFormLabel">Format <small>(Default: <i>{{editor.defFormatType.resolvedTitle}}</i>)</small></label>
<div class="hintbox" ng-if="editor.showFormatHelp">
<h4 class="hintbox-heading">
<i class="fa fa-warning text-warning"></i> Format Warning
</h4>
<p id="scriptFieldWarningCopy">
Formatting allows you to control the way that specific values are displayed. It can also cause values to be completely changed and prevent highlighting in Discover from working.
</p>
</div>
<select
ng-model="editor.selectedFormatId"
id="scriptFieldFormat"
ng-options="format.id as format.title for format in editor.fieldFormatTypes"
class="form-control"
aria-describedby="scriptFieldWarningCopy"
data-test-subj="editorSelectedFormatId">
</select>
<fieldset
field-format-editor
ng-if="editor.selectedFormatId"
field="editor.field"
format-params="editor.formatParams">
</fieldset>
</div>
<div class="form-group">
<label for="editorFieldCount" class="kuiFormLabel">Popularity</label>
<div class="kuiFieldGroup">
<div class="kuiFieldGroupSection">
<input
ng-model="editor.field.count"
id="editorFieldCount"
type="number"
class="form-control"
data-test-subj=editorFieldCount
>
</div>
<div class="kuiFieldGroupSection">
<div class="kuiButtonGroup kuiButtonGroup--united">
<button
data-test-subj="fieldIncreasePopularityButton"
type="button"
ng-click="editor.field.count = editor.field.count + 1"
aria-label="Increment popularity"
class="kuiButton kuiButton--basic"
>
<span
aria-hidden="true"
class="kuiIcon fa-plus"
></span>
</button>
<button
type="button"
ng-click="editor.field.count = editor.field.count - 1"
aria-label="Decrement popularity"
class="kuiButton kuiButton--basic"
>
<span
aria-hidden="true"
class="kuiIcon fa-minus"
></span>
</button>
</div>
</div>
</div>
</div>
<div ng-if="editor.field.scripted">
<div class="form-group">
<label for="scriptedFieldScript" class="kuiFormLabel">Script</label>
<textarea
required
class="field-editor_script-input form-control text-monospace"
id="scriptedFieldScript"
ng-model="editor.field.script"
data-test-subj="editorFieldScript"
></textarea>
</div>
<div class="euiCallOut euiCallOut--warning">
<div class="euiCallOutHeader">
<span class="euiCallOutHeader__title">Proceed with caution</span>
</div>
<div class="euiText euiText--small">
<p>
Please familiarize yourself with <a target="_window" documentation-href="scriptedFields.scriptFields">script fields <i class="fa-link fa"></i></a> and with <a target="_window" documentation-href="scriptedFields.scriptAggs">scripts in aggregations <i class="fa-link fa"></i></a> before using scripted fields.
</p>
<p>
Scripted fields can be used to display and aggregate calculated values. As such, they can be very slow, and if done incorrectly, can cause Kibana to be unusable. There's no safety net here. If you make a typo, unexpected exceptions will be thrown all over the place!
</p>
</div>
</div>
<div class="euiSpacer euiSpacer--m"></div>
<div class="euiCallOut euiCallOut--primary">
<div class="euiCallOutHeader">
<span class="euiCallOutHeader__title">Scripting Help</span>
</div>
<div class="euiText euiText--small">
<p>
By default, Kibana scripted fields use <a target="_window" documentation-href="scriptedFields.painless">Painless <i class="fa-link fa"></i></a>, a simple and secure scripting language designed specifically for use with Elasticsearch. To access values in the document use the following format:
</p>
<p><code>doc['some_field'].value</code></p>
<p>
Painless is powerful but easy to use. It provides access to many <a target="_window" documentation-href="scriptedFields.painlessApi">native Java APIs <i class="fa-link fa"></i></a>. Read up on its <a target="_window" documentation-href="scriptedFields.painlessSyntax">syntax <i class="fa-link fa"></i></a> and you'll be up to speed in no time!
</p>
<p>
Kibana currently imposes one special limitation on the painless scripts you write. They cannot contain named functions.
</p>
<p>
Coming from an older version of Kibana? The <a target="_window" documentation-href="scriptedFields.luceneExpressions">Lucene Expressions <i class="fa-link fa"></i></a> you know and love are still available. Lucene expressions are a lot like JavaScript, but limited to basic arithmetic, bitwise and comparison operations.
</p>
<p>
There are a few limitations when using Lucene Expressions:
</p>
<ul>
<li> Only numeric, boolean, date, and geo_point fields may be accessed </li>
<li> Stored fields are not available </li>
<li> If a field is sparse (only some documents contain a value), documents missing the field will have a value of 0 </li>
</ul>
<p>
Here are all the operations available to lucene expressions:
</p>
<ul>
<li> Arithmetic operators: + - * / % </li>
<li> Bitwise operators: | & ^ ~ << >> >>> </li>
<li> Boolean operators (including the ternary operator): && || ! ?: </li>
<li> Comparison operators: < <= == >= > </li>
<li> Common mathematic functions: abs ceil exp floor ln log10 logn max min sqrt pow </li>
<li> Trigonometric library functions: acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan </li>
<li> Distance functions: haversine </li>
<li> Miscellaneous functions: min, max </li>
</ul>
</div>
</div>
</div>
<div class="euiSpacer euiSpacer--l"></div>
<div ng-if="editor.conflictDescriptionsLength > 0">
<p>
<i class="fa fa-warning text-warning"></i>
<strong>Field Type Conflict:</strong>
The type of this field changes across indices. It is unavailable for many analysis functions. The indices per type are as follows:
<table class="table">
<thead>
<th scope="col"> Field Type </th>
<th scope="col"> Index Names </th>
</thead>
<tbody>
<tr ng-repeat="(type, indices) in editor.field.conflictDescriptions">
<td>{{type}}</td> <td>{{indices.join(', ')}}</td>
</tr>
</tbody>
</table>
</p>
</div>
<div class="form-group">
<button
type="button"
ng-if="editor.field.scripted && !editor.creating"
ng-click="editor.delete()"
aria-label="Delete"
class="kuiButton kuiButton--danger"
>
Delete Field
</button>
<button
data-test-subj="fieldSaveButton"
ng-disabled="form.$invalid"
type="submit"
aria-label="{{ editor.creating ? 'Create' : 'Update' }} Field"
class="kuiButton kuiButton--primary"
>
{{ editor.creating ? 'Create' : 'Update' }} Field
</button>
<button
data-test-subj="fieldCancelButton"
type="button"
ng-click="editor.cancel()"
class="kuiButton kuiButton--hollow"
>
Cancel
</button>
</div>
</form>

View file

@ -17,217 +17,522 @@
* under the License.
*/
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import { intersection, union, get } from 'lodash';
import '../field_format_editor';
import _ from 'lodash';
import { fieldFormats } from '../registry/field_formats';
import { IndexPatternsFieldProvider } from '../index_patterns/_field';
import { uiModules } from '../modules';
import fieldEditorTemplate from './field_editor.html';
import { toastNotifications } from '../notify';
import '../directives/documentation_href';
import './field_editor.less';
import {
GetEnabledScriptingLanguagesProvider,
getDeprecatedScriptingLanguages,
getSupportedScriptingLanguages,
getDeprecatedScriptingLanguages
} from '../scripting_languages';
import { getKbnTypeNames } from '../../../utils';
} from 'ui/scripting_languages';
uiModules
.get('kibana')
.directive('fieldEditor', function (Private, $sce, confirmModal, config) {
const getConfig = (...args) => config.get(...args);
import {
fieldFormats
} from 'ui/registry/field_formats';
const Field = Private(IndexPatternsFieldProvider);
const getEnabledScriptingLanguages = Private(GetEnabledScriptingLanguagesProvider);
import {
getDocLink
} from 'ui/documentation_links';
const fieldTypesByLang = {
painless: ['number', 'string', 'date', 'boolean'],
expression: ['number'],
default: getKbnTypeNames()
import {
toastNotifications
} from 'ui/notify';
import {
EuiButton,
EuiButtonEmpty,
EuiCode,
EuiConfirmModal,
EuiFieldNumber,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiIcon,
EuiLink,
EuiOverlayMask,
EuiSelect,
EuiSpacer,
EuiText,
EuiTextArea,
EUI_MODAL_CONFIRM_BUTTON,
} from '@elastic/eui';
import {
ScriptingDisabledCallOut,
ScriptingWarningCallOut,
ScriptingHelpFlyout,
} from './components/scripting_call_outs';
import {
FieldFormatEditor
} from './components/field_format_editor';
import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants';
import { copyField, getDefaultFormat } from './lib';
export class FieldEditor extends PureComponent {
static propTypes = {
indexPattern: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
helpers: PropTypes.shape({
Field: PropTypes.func.isRequired,
getConfig: PropTypes.func.isRequired,
$http: PropTypes.func.isRequired,
fieldFormatEditors: PropTypes.object.isRequired,
redirectAway: PropTypes.func.isRequired,
})
};
constructor(props) {
super(props);
const {
field,
indexPattern,
helpers: { Field },
} = props;
this.state = {
isReady: false,
isCreating: false,
isDeprecatedLang: false,
scriptingLangs: [],
fieldTypes: [],
fieldTypeFormats: [],
existingFieldNames: indexPattern.fields.map(f => f.name),
field: copyField(field, indexPattern, Field),
fieldFormatId: undefined,
fieldFormatParams: {},
showScriptingHelp: false,
showDeleteModal: false,
hasFormatError: false,
};
this.supportedLangs = getSupportedScriptingLanguages();
this.deprecatedLangs = getDeprecatedScriptingLanguages();
this.init();
}
return {
restrict: 'E',
template: fieldEditorTemplate,
scope: {
getIndexPattern: '&indexPattern',
getField: '&field'
},
controllerAs: 'editor',
controller: function ($scope, kbnUrl) {
const self = this;
async init() {
const { $http } = this.props.helpers;
const { field } = this.state;
const { indexPattern } = this.props;
getScriptingLangs().then((langs) => {
self.scriptingLangs = langs;
if (!_.includes(self.scriptingLangs, self.field.lang)) {
self.field.lang = undefined;
const getEnabledScriptingLanguages = new GetEnabledScriptingLanguagesProvider($http);
const enabledLangs = await getEnabledScriptingLanguages();
const scriptingLangs = intersection(enabledLangs, union(this.supportedLangs, this.deprecatedLangs));
field.lang = scriptingLangs.includes(field.lang) ? field.lang : undefined;
const fieldTypes = get(FIELD_TYPES_BY_LANG, field.lang, DEFAULT_FIELD_TYPES);
field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0];
const DefaultFieldFormat = fieldFormats.getDefaultType(field.type);
const fieldTypeFormats = [
getDefaultFormat(DefaultFieldFormat),
...fieldFormats.byFieldType[field.type],
];
this.setState({
isReady: true,
isCreating: !indexPattern.fields.byName[field.name],
isDeprecatedLang: this.deprecatedLangs.includes(field.lang),
errors: [],
scriptingLangs,
fieldTypes,
fieldTypeFormats,
fieldFormatId: get(indexPattern, ['fieldFormatMap', field.name, 'type', 'id']),
fieldFormatParams: field.format.params(),
});
}
onFieldChange = (fieldName, value) => {
const field = this.state.field;
field[fieldName] = value;
this.forceUpdate();
}
onTypeChange = (type) => {
const { getConfig } = this.props.helpers;
const { field } = this.state;
const DefaultFieldFormat = fieldFormats.getDefaultType(type);
field.type = type;
const fieldTypeFormats = [
getDefaultFormat(DefaultFieldFormat),
...fieldFormats.byFieldType[field.type],
];
const FieldFormat = fieldTypeFormats[0];
field.format = new FieldFormat(null, getConfig);
this.setState({
fieldTypeFormats,
fieldFormatId: FieldFormat.id,
fieldFormatParams: field.format.params(),
});
}
onLangChange = (lang) => {
const { field } = this.state;
const fieldTypes = get(FIELD_TYPES_BY_LANG, lang, DEFAULT_FIELD_TYPES);
field.lang = lang;
field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0];
this.setState({
fieldTypes,
});
}
onFormatChange = (formatId, params) => {
const { getConfig } = this.props.helpers;
const { field, fieldTypeFormats } = this.state;
const FieldFormat = fieldTypeFormats.find((format) => format.id === formatId) || fieldTypeFormats[0];
field.format = new FieldFormat(params, getConfig);
this.setState({
fieldFormatId: FieldFormat.id,
fieldFormatParams: field.format.params(),
});
}
onFormatParamsChange = (newParams) => {
const { fieldFormatId } = this.state;
this.onFormatChange(fieldFormatId, newParams);
}
onFormatParamsError = (error) => {
this.setState({
hasFormatError: !!error,
});
}
isDuplicateName() {
const { isCreating, field, existingFieldNames } = this.state;
return isCreating && existingFieldNames.includes(field.name);
}
renderName() {
const { isCreating, field } = this.state;
const isInvalid = !field.name || !field.name.trim();
return isCreating ? (
<EuiFormRow
label="Name"
helpText={this.isDuplicateName() ? (
<span>
<EuiIcon type="alert" color="warning" size="s" />&nbsp;
<strong>Mapping Conflict:</strong>&nbsp;
You already have a field with the name <EuiCode>{field.name}</EuiCode>. Naming your scripted
field with the same name means you won&apos;t be able to query both fields at the same time.
</span>
) : null}
isInvalid={isInvalid}
error={isInvalid ? 'Name is required' : null}
>
<EuiFieldText
value={field.name || ''}
placeholder="New scripted field"
data-test-subj="editorFieldName"
onChange={(e) => { this.onFieldChange('name', e.target.value); }}
isInvalid={isInvalid}
/>
</EuiFormRow>
) : null;
}
renderLanguage() {
const { field, scriptingLangs, isDeprecatedLang } = this.state;
return field.scripted ? (
<EuiFormRow
label="Language"
helpText={isDeprecatedLang ? (
<span>
<EuiIcon type="alert" color="warning" size="s" />&nbsp;
<strong>Deprecation Warning:</strong>&nbsp;
<EuiCode>{field.lang}</EuiCode> is deprecated and support will be removed in the
next major version of Kibana and Elasticsearch. We recommend using {(
<EuiLink target="_window" href={getDocLink('scriptedFields.painless')}>Painless</EuiLink>
)} for new scripted fields.
</span>
) : null}
>
<EuiSelect
value={field.lang}
options={scriptingLangs.map(lang => { return { value: lang, text: lang }; })}
data-test-subj="editorFieldLang"
onChange={(e) => { this.onLangChange(e.target.value); }}
/>
</EuiFormRow>
) : null;
}
renderType() {
const { field, fieldTypes } = this.state;
return (
<EuiFormRow label="Type">
<EuiSelect
value={field.type}
disabled={!field.scripted}
options={fieldTypes.map(type => { return { value: type, text: type }; })}
data-test-subj="editorFieldType"
onChange={(e) => {
this.onTypeChange(e.target.value);
}}
/>
</EuiFormRow>
);
}
renderFormat() {
const { field, fieldTypeFormats, fieldFormatId, fieldFormatParams } = this.state;
const { fieldFormatEditors } = this.props.helpers;
const defaultFormat = fieldTypeFormats[0] && fieldTypeFormats[0].resolvedTitle;
return (
<Fragment>
<EuiFormRow
label={<span>Format {defaultFormat ? <span>(Default: <EuiCode>{defaultFormat}</EuiCode>)</span> : null}</span>}
helpText={
<span>
Formatting allows you to control the way that specific values are displayed.
It can also cause values to be completely changed and prevent highlighting in Discover from working.
</span>
}
});
>
<EuiSelect
value={fieldFormatId}
options={fieldTypeFormats.map(format => { return { value: format.id || '', text: format.title }; })}
data-test-subj="editorSelectedFormatId"
onChange={(e) => { this.onFormatChange(e.target.value); }}
/>
</EuiFormRow>
{ fieldFormatId ? (
<FieldFormatEditor
fieldType={field.type}
fieldFormat={field.format}
fieldFormatId={fieldFormatId}
fieldFormatParams={fieldFormatParams}
fieldFormatEditors={fieldFormatEditors}
onChange={this.onFormatParamsChange}
onError={this.onFormatParamsError}
/>
) : null }
</Fragment>
);
}
self.indexPattern = $scope.getIndexPattern();
self.field = shadowCopy($scope.getField());
self.formatParams = self.field.format.params();
self.conflictDescriptionsLength = (self.field.conflictDescriptions) ? Object.keys(self.field.conflictDescriptions).length : 0;
renderPopularity() {
const { field } = this.state;
// only init on first create
self.creating = !self.indexPattern.fields.byName[self.field.name];
self.existingFieldNames = self.indexPattern.fields.map(field => field.name); //used for mapping conflict validation
self.selectedFormatId = _.get(self.indexPattern, ['fieldFormatMap', self.field.name, 'type', 'id']);
self.defFormatType = initDefaultFormat();
return (
<EuiFormRow label="Popularity">
<EuiFieldNumber
value={field.count}
data-test-subj="editorFieldCount"
onChange={(e) => { this.onFieldChange('count', e.target.value ? Number(e.target.value) : '');}}
/>
</EuiFormRow>
);
}
self.cancel = redirectAway;
self.save = function () {
const indexPattern = self.indexPattern;
const fields = indexPattern.fields;
const field = self.field.toActualField();
renderScript() {
const { field } = this.state;
const isInvalid = !field.script || !field.script.trim();
const index = fields.findIndex(f => f.name === field.name);
if (index > -1) {
fields.splice(index, 1, field);
} else {
fields.push(field);
}
return field.scripted ? (
<EuiFormRow
label="Script"
helpText={(<EuiLink onClick={this.showScriptingHelp}>Scripting help</EuiLink>)}
isInvalid={isInvalid}
error={isInvalid ? 'Script is required' : null}
>
<EuiTextArea
value={field.script}
data-test-subj="editorFieldScript"
onChange={(e) => { this.onFieldChange('script', e.target.value); }}
isInvalid={isInvalid}
/>
</EuiFormRow>
) : null;
}
if (!self.selectedFormatId) {
indexPattern.fieldFormatMap[field.name] = {};
} else {
indexPattern.fieldFormatMap[field.name] = self.field.format;
}
showScriptingHelp = () => {
this.setState({
showScriptingHelp: true
});
}
return indexPattern.save()
.then(function () {
toastNotifications.addSuccess(`Saved '${self.field.name}'`);
redirectAway();
});
};
hideScriptingHelp = () => {
this.setState({
showScriptingHelp: false
});
}
self.delete = function () {
function doDelete() {
const indexPattern = self.indexPattern;
const field = self.field;
renderDeleteModal = () => {
const { field } = this.state;
indexPattern.fields.remove({ name: field.name });
return indexPattern.save()
.then(function () {
toastNotifications.addSuccess(`Deleted '${self.field.name}'`);
redirectAway();
});
}
const confirmModalOptions = {
confirmButtonText: 'Delete',
onConfirm: doDelete,
title: `Delete field '${self.field.name}'?`
};
confirmModal(
`You can't recover a deleted field.`,
confirmModalOptions
);
};
return this.state.showDeleteModal ? (
<EuiOverlayMask>
<EuiConfirmModal
title={`Delete field '${field.name}'`}
onCancel={this.hideDeleteModal}
onConfirm={() => {
this.hideDeleteModal();
this.deleteField();
}}
cancelButtonText="Cancel"
confirmButtonText="Delete"
buttonColor="danger"
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
>
<p>You can&apos;t recover a deleted field.</p>
<p>Are you sure you want to do this?</p>
</EuiConfirmModal>
</EuiOverlayMask>
) : null;
}
self.isDeprecatedLang = function (lang) {
return _.contains(getDeprecatedScriptingLanguages(), lang);
};
showDeleteModal = () => {
this.setState({
showDeleteModal: true
});
}
$scope.$watch('editor.selectedFormatId', function (cur, prev) {
const format = self.field.format;
const changedFormat = cur !== prev;
const missingFormat = cur && (!format || format.type.id !== cur);
hideDeleteModal = () => {
this.setState({
showDeleteModal: false
});
}
if (!changedFormat || !missingFormat) {
return;
}
renderActions() {
const { isCreating, field } = this.state;
const { redirectAway } = this.props.helpers;
// reset to the defaults, but make sure it's an object
const FieldFormat = getFieldFormatType();
const paramDefaults = new FieldFormat({}, getConfig).getParamDefaults();
const currentFormatParams = self.formatParams;
self.formatParams = _.assign({}, _.cloneDeep(paramDefaults));
// If there are no current or new params, the watch will not trigger
// so manually update the format here
if (_.size(currentFormatParams) === 0 && _.size(self.formatParams) === 0) {
self.field.format = new FieldFormat(self.formatParams, getConfig);
}
});
$scope.$watch('editor.formatParams', function () {
const FieldFormat = getFieldFormatType();
self.field.format = new FieldFormat(self.formatParams, getConfig);
}, true);
$scope.$watch('editor.field.type', function (newValue) {
self.defFormatType = initDefaultFormat();
self.fieldFormatTypes = [self.defFormatType].concat(fieldFormats.byFieldType[newValue] || []);
if (_.isUndefined(_.find(self.fieldFormatTypes, { id: self.selectedFormatId }))) {
delete self.selectedFormatId;
}
});
$scope.$watch('editor.field.lang', function (newValue) {
self.fieldTypes = _.get(fieldTypesByLang, newValue, fieldTypesByLang.default);
if (!_.contains(self.fieldTypes, self.field.type)) {
self.field.type = _.first(self.fieldTypes);
}
});
// copy the defined properties of the field to a plain object
// which is mutable, and capture the changed separately.
function shadowCopy(field) {
const changes = {};
const shadowProps = {
toActualField: {
// bring the shadow copy out of the shadows
value: function toActualField() {
return new Field(self.indexPattern, _.defaults({}, changes, field.$$spec));
}
}
};
Object.getOwnPropertyNames(field).forEach(function (prop) {
const desc = Object.getOwnPropertyDescriptor(field, prop);
shadowProps[prop] = {
enumerable: desc.enumerable,
get: function () {
return _.has(changes, prop) ? changes[prop] : field[prop];
},
set: function (v) {
changes[prop] = v;
}
};
});
return Object.create(null, shadowProps);
return (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={this.saveField}
isDisabled={this.isSavingDisabled()}
data-test-subj="fieldSaveButton"
>
{isCreating ? 'Create field' : 'Save field'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={redirectAway}
data-test-subj="fieldCancelButton"
>
Cancel
</EuiButtonEmpty>
</EuiFlexItem>
{
!isCreating && field.scripted ? (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="danger"
onClick={this.showDeleteModal}
>
Delete
</EuiButtonEmpty>
</EuiFlexItem>
) : null
}
</EuiFlexGroup>
);
}
function redirectAway() {
kbnUrl.changeToRoute(self.indexPattern, self.field.scripted ? 'scriptedFields' : 'indexedFields');
}
deleteField = () => {
const { redirectAway } = this.props.helpers;
const { indexPattern } = this.props;
const { field } = this.state;
const remove = indexPattern.removeScriptedField(field.name);
function getFieldFormatType() {
if (self.selectedFormatId) return fieldFormats.getType(self.selectedFormatId);
else return fieldFormats.getDefaultType(self.field.type);
}
if(remove) {
remove.then(() => {
toastNotifications.addSuccess(`Deleted '${field.name}'`);
redirectAway();
});
} else {
redirectAway();
}
}
function getScriptingLangs() {
return getEnabledScriptingLanguages()
.then((enabledLanguages) => {
return _.intersection(enabledLanguages, _.union(getSupportedScriptingLanguages(), getDeprecatedScriptingLanguages()));
});
}
saveField = () => {
const { redirectAway } = this.props.helpers;
const { indexPattern } = this.props;
const { fieldFormatId } = this.state;
function initDefaultFormat() {
const def = Object.create(fieldFormats.getDefaultType(self.field.type));
const field = this.state.field.toActualField();
const index = indexPattern.fields.findIndex(f => f.name === field.name);
// explicitly set to undefined to prevent inheriting the prototypes id
def.id = undefined;
def.resolvedTitle = def.title;
def.title = '- default - ';
if (index > -1) {
indexPattern.fields.splice(index, 1, field);
} else {
indexPattern.fields.push(field);
}
return def;
}
}
};
});
if (!fieldFormatId) {
indexPattern.fieldFormatMap[field.name] = {};
} else {
indexPattern.fieldFormatMap[field.name] = field.format;
}
return indexPattern.save()
.then(function () {
toastNotifications.addSuccess(`Saved '${field.name}'`);
redirectAway();
});
}
isSavingDisabled() {
const { field, hasFormatError } = this.state;
if(
hasFormatError
|| !field.name
|| !field.name.trim()
|| (field.scripted && (!field.script || !field.script.trim()))
) {
return true;
}
return false;
}
render() {
const { isReady, isCreating, scriptingLangs, field, showScriptingHelp } = this.state;
return isReady ? (
<div>
<EuiText>
<h3>{isCreating ? 'Create scripted field' : `Edit ${field.name}`}</h3>
</EuiText>
<EuiSpacer size="m" />
<EuiForm>
<ScriptingDisabledCallOut isVisible={field.scripted && !scriptingLangs.length} />
<ScriptingWarningCallOut isVisible={field.scripted} />
<ScriptingHelpFlyout
isVisible={field.scripted && showScriptingHelp}
onClose={this.hideScriptingHelp}
/>
{this.renderName()}
{this.renderLanguage()}
{this.renderType()}
{this.renderFormat()}
{this.renderPopularity()}
{this.renderScript()}
{this.renderActions()}
{this.renderDeleteModal()}
</EuiForm>
<EuiSpacer size="l" />
</div>
) : null;
}
}

View file

@ -1,3 +0,0 @@
textarea.field-editor_script-input {
height: 100px;
}

View file

@ -0,0 +1,201 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { FieldEditor } from './field_editor';
jest.mock('@elastic/eui', () => ({
EuiButton: 'eui-button',
EuiButtonEmpty: 'eui-button-empty',
EuiCode: 'eui-code',
EuiConfirmModal: 'eui-confirm-modal',
EuiFieldNumber: 'eui-field-number',
EuiFieldText: 'eui-field-text',
EuiFlexGroup: 'eui-flex-group',
EuiFlexItem: 'eui-flex-item',
EuiForm: 'eui-form',
EuiFormRow: 'eui-form-row',
EuiIcon: 'eui-icon',
EuiLink: 'eui-link',
EuiOverlayMask: 'eui-overlay-mask',
EuiSelect: 'eui-select',
EuiSpacer: 'eui-spacer',
EuiText: 'eui-text',
EuiTextArea: 'eui-textArea',
}));
jest.mock('ui/scripting_languages', () => ({
GetEnabledScriptingLanguagesProvider: jest.fn().mockImplementation(() => () => ['painless', 'testlang']),
getSupportedScriptingLanguages: () => ['painless'],
getDeprecatedScriptingLanguages: () => ['testlang'],
}));
jest.mock('ui/registry/field_formats', () => {
class Format {
static id = 'test_format'; static title = 'Test format';
params() {}
}
return {
fieldFormats: {
getDefaultType: () => {
return Format;
},
byFieldType: {
'number': [Format],
},
},
};
});
jest.mock('ui/documentation_links', () => ({
getDocLink: (doc) => `(docLink for ${doc})`,
}));
jest.mock('ui/notify', () => ({
toastNotifications: {
addSuccess: jest.fn(),
}
}));
jest.mock('./components/scripting_call_outs', () => ({
ScriptingDisabledCallOut: 'scripting-disabled-callOut',
ScriptingWarningCallOut: 'scripting-warning-callOut',
ScriptingHelpFlyout: 'scripting-help-flyout',
}));
jest.mock('./components/field_format_editor', () => ({
FieldFormatEditor: 'field-format-editor'
}));
const fields = [{
name: 'foobar',
}];
fields.byName = {
foobar: {
name: 'foobar',
},
};
class Format {
static id = 'test_format'; static title = 'Test format';
params() {}
}
const field = {
scripted: true,
type: 'number',
lang: 'painless',
format: new Format(),
};
const helpers = {
Field: () => {},
getConfig: () => {},
$http: () => {},
fieldFormatEditors: {},
redirectAway: () => {},
};
describe('FieldEditor', () => {
let indexPattern;
beforeEach(() => {
indexPattern = {
fields,
};
});
it('should render create new scripted field correctly', async () => {
const component = shallow(
<FieldEditor
indexPattern={indexPattern}
field={field}
helpers={helpers}
/>
);
await new Promise(resolve => process.nextTick(resolve));
component.update();
expect(component).toMatchSnapshot();
});
it('should render edit scripted field correctly', async () => {
const testField = {
...field,
name: 'test',
script: 'doc.test.value',
};
indexPattern.fields.push(testField);
indexPattern.fields.byName[testField.name] = testField;
const component = shallow(
<FieldEditor
indexPattern={indexPattern}
field={testField}
helpers={helpers}
/>
);
await new Promise(resolve => process.nextTick(resolve));
component.update();
expect(component).toMatchSnapshot();
});
it('should show deprecated lang warning', async () => {
const testField = {
...field,
name: 'test',
script: 'doc.test.value',
lang: 'testlang'
};
indexPattern.fields.push(testField);
indexPattern.fields.byName[testField.name] = testField;
const component = shallow(
<FieldEditor
indexPattern={indexPattern}
field={testField}
helpers={helpers}
/>
);
await new Promise(resolve => process.nextTick(resolve));
component.update();
expect(component).toMatchSnapshot();
});
it('should show conflict field warning', async () => {
const testField = { ...field };
const component = shallow(
<FieldEditor
indexPattern={indexPattern}
field={testField}
helpers={helpers}
/>
);
await new Promise(resolve => process.nextTick(resolve));
component.instance().onFieldChange('name', 'foobar');
component.update();
expect(component).toMatchSnapshot();
});
});

View file

@ -17,4 +17,4 @@
* under the License.
*/
import './field_editor';
export { FieldEditor } from './field_editor';

View file

@ -0,0 +1,52 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { convertSampleInput } from '../convert_sample_input';
const converter = (input) => {
if(isNaN(input)) {
throw {
message: 'Input is not a number'
};
} else {
return input * 2;
}
};
describe('convertSampleInput', () => {
it('should convert a set of inputs', () => {
const inputs = [1, 10, 15];
const output = convertSampleInput(converter, inputs);
expect(output.error).toEqual(null);
expect(JSON.stringify(output.samples)).toEqual(JSON.stringify([
{ input: 1, output: 2 },
{ input: 10, output: 20 },
{ input: 15, output: 30 },
]));
});
it('should return error if converter throws one', () => {
const inputs = [1, 10, 15, 'invalid'];
const output = convertSampleInput(converter, inputs);
expect(output.error).toEqual('An error occurred while trying to use this format configuration: Input is not a number');
expect(JSON.stringify(output.samples)).toEqual(JSON.stringify([]));
});
});

View file

@ -0,0 +1,48 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { copyField } from '../copy_field';
const field = {
name: 'test_field',
scripted: true,
type: 'number',
lang: 'painless',
};
describe('copyField', () => {
it('should copy a field', () => {
const copiedField = copyField(field, {}, {});
copiedField.name = 'test_name_change';
// Check that copied field has `toActualField()` method
expect(typeof copiedField.toActualField).toEqual('function');
// Check that we did not modify the original field object when
// modifying copied field
expect(field.toActualField).toEqual(undefined);
expect(field.name).toEqual('test_field');
expect(copiedField).not.toEqual(field);
expect(copiedField.name).toEqual('test_name_change');
expect(copiedField.scripted).toEqual(field.scripted);
expect(copiedField.type).toEqual(field.type);
expect(copiedField.lang).toEqual(field.lang);
});
});

Some files were not shown because too many files have changed in this diff Show more