Field editor to React/EUI (#20245)
|
@ -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' },
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './samples';
|
||||
import './create_edit_field';
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
`;
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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 />`;
|
|
@ -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>
|
||||
`;
|
|
@ -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],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { durationEditor } from './duration';
|
||||
export { BytesFormatEditor } from './bytes';
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { colorEditor } from './color';
|
||||
export { ColorFormatEditor } from './color';
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { stringEditor } from './string';
|
||||
export { DateFormatEditor } from './date';
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DefaultFormatEditor should render nothing 1`] = `""`;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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>
|
||||
`;
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { dateEditor } from './date';
|
||||
export { NumberFormatEditor } from './number';
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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';
|
|
@ -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],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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';
|
|
@ -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
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
Before Width: | Height: | Size: 802 B After Width: | Height: | Size: 802 B |
Before Width: | Height: | Size: 124 B After Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
@ -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';
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
Before Width: | Height: | Size: 919 B After Width: | Height: | Size: 919 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './scripted_field_editor';
|
||||
export { UrlFormatEditor } from './url';
|
|
@ -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> — The fields value
|
||||
</li>
|
||||
<li>
|
||||
<EuiCode>url</EuiCode> — 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';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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> — The URI-escaped value
|
||||
</li>
|
||||
<li>
|
||||
<EuiCode>rawValue</EuiCode> — 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';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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);
|
|
@ -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`] = `""`;
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.fieldFormatEditor__samples {
|
||||
audio {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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`] = `""`;
|
|
@ -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: | & ^ ~ << >> >>>
|
||||
</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: haversin
|
||||
</li>
|
||||
<li>
|
||||
Miscellaneous functions: min, max
|
||||
</li>
|
||||
</ul>
|
||||
</EuiText>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
`;
|
||||
|
||||
exports[`ScriptingHelpFlyout should render nothing if not visible 1`] = `""`;
|
|
@ -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`] = `""`;
|
|
@ -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';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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['some_field'].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'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: | & ^ ~ << >> >>></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: haversin</li>
|
||||
<li> Miscellaneous functions: min, max</li>
|
||||
</ul>
|
||||
</EuiText>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
) : null;
|
||||
};
|
||||
|
||||
ScriptingHelpFlyout.displayName = 'ScriptingHelpFlyout';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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'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';
|
|
@ -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();
|
||||
});
|
||||
});
|
27
src/ui/public/field_editor/constants/index.js
Normal 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();
|
|
@ -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>
|
|
@ -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" />
|
||||
<strong>Mapping Conflict:</strong>
|
||||
You already have a field with the name <EuiCode>{field.name}</EuiCode>. Naming your scripted
|
||||
field with the same name means you won'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" />
|
||||
<strong>Deprecation Warning:</strong>
|
||||
<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'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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
textarea.field-editor_script-input {
|
||||
height: 100px;
|
||||
}
|
201
src/ui/public/field_editor/field_editor.test.js
Normal 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();
|
||||
});
|
||||
});
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './field_editor';
|
||||
export { FieldEditor } from './field_editor';
|
||||
|
|
|
@ -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([]));
|
||||
});
|
||||
});
|
48
src/ui/public/field_editor/lib/__tests__/copy_field.test.js
Normal 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);
|
||||
});
|
||||
});
|