[settings/indices] merged the scripted field editor and the normal field editor

This commit is contained in:
Spencer Alger 2015-04-21 14:30:49 -07:00
parent 0883e8e349
commit 631eb95f7a
19 changed files with 184 additions and 301 deletions

View file

@ -1,24 +1,11 @@
<form ng-submit="editor.save()" name="form">
<h2 ng-if="editor.creating"><strong>Create {{ editor.field.scripted ? 'Scripted ' : '' }}Field</strong></h1>
<h2 ng-if="!editor.creating"><strong>Field:</strong> <code>{{ editor.field.name }}</code></h1>
<div class="form-group">
<label>
Index Pattern
<a href="#{{ editor.indexPattern.editRoute }}"><i class="fa fa-pencil" aria-label="edit"></i></a>
</label>
<input
ng-model="editor.indexPattern.id"
readonly
class="form-control"
>
</div>
<div class="form-group">
<div ng-if="editor.creating" class="form-group">
<label>Name</label>
<input
ng-model="editor.field.name"
ng-readonly="!editor.creating"
ng-model="editor.fieldSpec.name"
required
placeholder="New Scripted Field"
input-focus
class="form-control"
>
</div>
@ -32,18 +19,6 @@
>
</div>
<div class="form-group" ng-if="editor.field.scripted">
<label>
Script
<small>Please familiarize yourself with <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html#search-request-script-fields">script fields <i class="fa-link fa"></i></a> and with <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-script">scripts in aggregations <i class="fa-link fa"></i></a> before using scripted fields.</small>
</label>
<div ng-bind-html="editor.scriptingInfo" class="hintbox"></div>
<div ng-bind-html="editor.scriptingWarning" class="hintbox"></div>
<textarea required class="form-control text-monospace" ng-model="editor.field.script"></textarea>
</div>
<div class="form-group">
<label>Format <small>(Default: <i>{{editor.defFormatType.resolvedTitle}}</i>)</small></label>
<select
@ -82,6 +57,17 @@
</div>
</div>
<div class="form-group" ng-if="editor.field.scripted">
<label>Script</label>
<textarea required class="form-control text-monospace" ng-model="editor.field.script"></textarea>
</div>
<div class="form-group">
<div ng-bind-html="editor.scriptingWarning" class="hintbox"></div>
</div>
<div class="form-group">
<div ng-bind-html="editor.scriptingInfo" class="hintbox"></div>
</div>
<div class="form-group">
<button
ng-click="editor.cancel()"

View file

@ -1,6 +1,6 @@
define(function (require) {
require('plugins/field_editor/field_editor_format_fieldset');
require('components/field_editor/field_editor_format_fieldset');
require('modules')
.get('app/settings')
@ -11,9 +11,9 @@ define(function (require) {
return {
restrict: 'E',
template: require('text!plugins/field_editor/field_editor.html'),
template: require('text!components/field_editor/field_editor.html'),
controllerAs: 'editor',
controller: function ($sce, $scope, $attrs, Notifier) {
controller: function ($sce, $scope, $attrs, Notifier, kbnUrl) {
var self = this;
var notify = new Notifier({ location: 'Field Editor' });
@ -25,14 +25,14 @@ define(function (require) {
self.formatParams = self.field.format.params() || {};
self.defFormatType = initDefaultFormat();
self.scriptingInfo = $sce.trustAsHtml(require('text!plugins/field_editor/scripting_info.html'));
self.scriptingWarning = $sce.trustAsHtml(require('text!plugins/field_editor/scripting_warning.html'));
self.scriptingInfo = $sce.trustAsHtml(require('text!components/field_editor/scripting_info.html'));
self.scriptingWarning = $sce.trustAsHtml(require('text!components/field_editor/scripting_warning.html'));
self.fieldFormatTypes = [self.defFormatType].concat(fieldFormats.byFieldType[self.field.type] || []);
self.creating = !self.indexPattern.fields.byName[self.field.name];
self.cancel = function () {
window.location.hash = self.indexPattern.editRoute;
kbnUrl.change(self.indexPattern.editRoute);
};
self.save = function () {
@ -56,13 +56,14 @@ define(function (require) {
return indexPattern.save()
.then(function () {
notify.info('Saved Field "' + self.field.name + '"');
window.location.hash = self.indexPattern.editRoute;
kbnUrl.change(self.indexPattern.editRoute);
});
};
$scope.$watchMulti([
'editor.selectedFormatId',
'=editor.formatParams'
'=editor.formatParams',
'=editor.fieldSpec'
], function (cur) {
var id = cur[0];
var field = self.field;

View file

@ -0,0 +1,11 @@
<h4>
<i class="fa fa-warning text-warning"></i> Proceed with caution
</h4>
<p>
Please familiarize yourself with <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html#search-request-script-fields">script fields <i class="fa-link fa"></i></a> and with <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-script">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>

View file

@ -1,8 +0,0 @@
<div class="container">
<field-editor
index-pattern="fieldEditorPage.indexPattern"
field="fieldEditorPage.field">
</field-editor>
</div>

View file

@ -1,58 +0,0 @@
define(function (require) {
var _ = require('lodash');
require('plugins/field_editor/field_editor');
require('routes')
.when('/settings/indices/:indexPatternId/field/:fieldName', {
mode: 'edit',
template: require('text!plugins/field_editor/index.html'),
resolve: {
indexPattern: function ($route, courier) {
return courier.indexPatterns.get($route.current.params.indexPatternId)
.catch(courier.redirectWhenMissing('/settings/indices'));
}
},
controllerAs: 'fieldEditorPage',
controller: FieldEditorPageController
})
.when('/settings/indices/:indexPatternId/create-scripted-field', {
mode: 'create-scripted',
template: require('text!plugins/field_editor/index.html'),
resolve: {
indexPattern: function ($route, courier) {
return courier.indexPatterns.get($route.current.params.indexPatternId)
.catch(courier.redirectWhenMissing('/settings/indices'));
}
},
controllerAs: 'fieldEditorPage',
controller: FieldEditorPageController
});
function FieldEditorPageController($route, Private, Notifier, kbnUrl) {
var Field = Private(require('components/index_patterns/_field'));
var notify = new Notifier({ location: 'Field Editor' });
this.mode = $route.current.mode;
_.assign(this, $route.current.locals);
if (this.mode === 'edit') {
this.fieldName = $route.current.params.fieldName;
this.field = this.indexPattern.fields.byName[this.fieldName];
if (!this.field) {
notify.error(this.indexPattern + ' does not have a "' + this.fieldName + '" field.');
kbnUrl.redirect(this.indexPattern.editRoute);
return;
}
}
else if (this.mode === 'create-scripted') {
this.field = new Field(this.indexPattern, {
scripted: this.mode === 'create-scripted',
type: 'number',
name: 'New Scripted Field'
});
}
}
});

View file

@ -1,6 +0,0 @@
<h4>
<i class="fa fa-warning text-warning"></i> Proceed with caution
</h4>
<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>

View file

@ -0,0 +1,15 @@
<kbn-settings-app section="indices">
<kbn-settings-indices>
<h1>{{ editor.indexPattern.id }}</h1>
<h2 ng-if="fieldSettings.mode === 'create'">Create {{ editor.field.scripted ? 'Scripted ' : '' }}Field</h2>
<h2 ng-if="fieldSettings.mode === 'edit'">{{ editor.field.name }}</h2>
<field-editor
index-pattern="fieldSettings.indexPattern"
field="fieldSettings.field"
cancel="goBack()">
</field-editor>
</kbn-settings-indices>
</kbn-settings-app>

View file

@ -0,0 +1,108 @@
define(function (require) {
var _ = require('lodash');
require('components/field_editor/field_editor');
require('plugins/settings/sections/indices/_index_header');
require('routes')
.when('/settings/indices/:indexPatternId/field/:fieldName', { mode: 'edit' })
.when('/settings/indices/:indexPatternId/create-field/', { mode: 'create' })
.defaults(/settings\/indices\/[^\/]+\/(field|create-field)(\/|$)/, {
template: require('text!plugins/settings/sections/indices/_field_editor.html'),
resolve: {
indexPattern: function ($route, courier) {
return courier.indexPatterns.get($route.current.params.indexPatternId)
.catch(courier.redirectWhenMissing('/settings/indices'));
}
},
controllerAs: 'fieldSettings',
controller: function FieldEditorPageController($route, Private, Notifier, kbnUrl) {
var Field = Private(require('components/index_patterns/_field'));
var notify = new Notifier({ location: 'Field Editor' });
this.mode = $route.current.mode;
this.indexPattern = $route.current.locals.indexPattern;
if (this.mode === 'edit') {
var fieldName = $route.current.params.fieldName;
this.field = this.indexPattern.fields.byName[fieldName];
if (!this.field) {
notify.error(this.indexPattern + ' does not have a "' + fieldName + '" field.');
kbnUrl.redirect(this.indexPattern.editRoute);
return;
}
}
else if (this.mode === 'create') {
this.field = new Field(this.indexPattern, {
scripted: true,
type: 'number'
});
} else {
throw new Error('unknown fieldEditorPage mode ' + this.mode);
}
this.goBack = function () {
kbnUrl.change(this.indexPattern.editUrl);
};
}
});
require('modules').get('apps/settings')
.controller('scriptedFieldsEdit', function ($scope, $route, $window, Notifier, Private) {
var fieldTypes = Private(require('components/index_patterns/_field_types'));
var notify = new Notifier();
var createMode = (!$route.current.params.field);
$scope.indexPattern = $route.current.locals.indexPattern;
$scope.fieldTypes = fieldTypes;
if (createMode) {
$scope.action = 'Create';
} else {
var scriptName = $route.current.params.field;
$scope.action = 'Edit';
$scope.scriptedField = _.find($scope.indexPattern.fields, {
name: scriptName,
scripted: true
});
}
$scope.submit = function () {
var field = _.defaults($scope.scriptedField, {
type: 'number',
lang: 'expression'
});
try {
if (createMode) {
$scope.indexPattern.addScriptedField(field.name, field.script, field.type, field.lang);
} else {
$scope.indexPattern.save();
}
notify.info('Scripted field \'' + $scope.scriptedField.name + '\' successfully saved');
$scope.goBack();
} catch (e) {
notify.error(e.message);
}
};
$scope.$watch('scriptedField.name', function (name) {
checkConflict(name);
});
function checkConflict(name) {
var match = _.find($scope.indexPattern.getFields(), {
name: name
});
if (match) {
$scope.namingConflict = true;
} else {
$scope.namingConflict = false;
}
}
});
});

View file

@ -1,8 +0,0 @@
<span ng-show="$parent.editFormat !== field.name">{{field.format.name}}</span>
<i class="fa fa-pencil text-muted pull-right cell-hover-show"></i>
<select
ng-show="$parent.editFormat === field.name"
ng-click="$event.stopPropagation()"
ng-options="name for name in formatOptionNames"
ng-model="selectedFormat">
</select>

View file

@ -3,19 +3,18 @@ define(function (require) {
require('components/paginated_table/paginated_table');
require('modules').get('apps/settings')
.directive('indexedFields', function (Private) {
.directive('indexedFields', function () {
var yesTemplate = '<i class="fa fa-check" aria-label="yes"></i>';
var noTemplate = '';
var nameHtml = require('text!plugins/settings/sections/indices/_field_name.html');
var typeHtml = require('text!plugins/settings/sections/indices/_field_type.html');
var formatHtml = require('text!plugins/settings/sections/indices/_field_format.html');
var controlsHtml = require('text!plugins/settings/sections/indices/_field_controls.html');
return {
restrict: 'E',
template: require('text!plugins/settings/sections/indices/_indexed_fields.html'),
scope: true,
link: function ($scope, $el, attr) {
link: function ($scope) {
var rowScopes = []; // track row scopes, so they can be destroyed as needed
$scope.perPage = 25;
$scope.columns = [

View file

@ -2,10 +2,10 @@
<p>These scripted fields are computed on the fly from your data. They can be used in visualizations and displayed in your documents, however they can not be searched. You can manage them here and add new ones as you see fit, but be careful, scripts can be tricky! <!-- If you need some examples, why not let Kibana <a ng-click="addDateScripts()"><strong>create a few examples from your date fields.</strong></a --></p>
<header>
<button ng-click="create()" class="btn btn-info" aria-label="Add Scripted Field">
<a ng-href="{{ '#' + indexPattern.addFieldRoute }} " class="btn btn-info" aria-label="Add Scripted Field">
<i aria-hidden="true" class="fa fa-plus"></i>
Add Scripted Field
</button>
</a>
</header>
<paginated-table

View file

@ -35,4 +35,4 @@
</div>
</div>
<div class="col-md-10" ng-transclude></div>
<div class="col-md-10" ng-transclude></div>

View file

@ -1,30 +1,32 @@
define(function (require) {
var _ = require('lodash');
require('plugins/settings/sections/indices/scripted_fields/index');
require('plugins/settings/sections/indices/_create');
require('plugins/settings/sections/indices/_edit');
require('plugins/settings/sections/indices/_field_editor');
// add a dependency to all of the subsection routes
require('routes')
.addResolves(/settings\/indices/, {
indexPatternIds: function (courier) {
return courier.indexPatterns.getIds();
.defaults(/settings\/indices/, {
resolve: {
indexPatternIds: function (courier) {
return courier.indexPatterns.getIds();
}
}
});
// wrapper directive, which sets some global stuff up like the left nav
require('modules').get('apps/settings')
.directive('kbnSettingsIndices', function ($route, config, kbnUrl, $rootScope) {
.directive('kbnSettingsIndices', function ($route, config, kbnUrl) {
return {
restrict: 'E',
transclude: true,
template: require('text!plugins/settings/sections/indices/index.html'),
link: function ($scope) {
$scope.edittingId = $route.current.params.id;
$scope.edittingId = $route.current.params.indexPatternId;
config.$bind($scope, 'defaultIndex');
$scope.$watch('defaultIndex', function (defaultIndex) {
$scope.$watch('defaultIndex', function () {
$scope.indexPatternList = _($route.current.locals.indexPatternIds)
.map(function (id) {
return {

View file

@ -1,70 +0,0 @@
<kbn-settings-app section="indices">
<kbn-settings-indices>
<div ng-controller="scriptedFieldsEdit">
<h1>{{ action }} Scripted Field</h1>
<div>
<p>
By default, Elasticsearch scripts use <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_lucene_expressions_scripts">Lucene Expressions <i class="fa-link fa"></i></a>, which is a lot like JavaScript, but limited to basic arithmetic, bitwise and comparison operations. We'll let you do some reading on <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_lucene_expressions_scripts">Lucene Expressions<i class="fa-link fa"></i></a> To access values in the document use the following format:
<h4><code>doc['some_field'].value</code></h4>
</p>
<p>There are a few limitations when using Lucene Expressions:
<ul>
<li>Only numeric 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>
<p>
Here are all the operations available to scripted fields:
<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>
</div>
<div class="bs-callout bs-callout-warning">
<h4>Proceed with caution</h4>
<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>
<p></p>
</div>
<form name="scriptedFieldForm" ng-submit="submit()">
<div class="form-group">
<label>Name</label>
<input required type="text" ng-model="scriptedField.name" class="form-control span12">
</div>
<div class="form-group">
<label>Script <small>Please familiarize yourself with <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html#search-request-script-fields">script fields <i class="fa-link fa"></i></a> and with
<a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-script">scripts in aggregations <i class="fa-link fa"></i></a>
before using scripted fields.</small></label>
<textarea required class="scripted-field-script form-control span12" ng-model="scriptedField.script"></textarea>
</div>
<div ng-if="namingConflict" class="alert alert-danger">
You already have a field with the name {{ scriptedField.name }}. Naming your scripted
field with the same name means you won't be able to query both fields at the same time.
</div>
<div class="form-group">
<button class="btn btn-primary" type="button" ng-click="goBack()">Cancel</button>
<button class="btn btn-success" type="submit">
Save Scripted Field
</button>
</div>
</form>
</div>
</kbn-settings-indices>
</kbn-settings-app>

View file

@ -1,84 +0,0 @@
define(function (require) {
var _ = require('lodash');
require('plugins/settings/sections/indices/_indexed_fields');
require('plugins/settings/sections/indices/_scripted_fields');
require('routes')
.addResolves(/settings\/indices\/(.+)\/scriptedField/, {
indexPattern: function ($route, courier) {
return courier.indexPatterns.get($route.current.params.id)
.catch(courier.redirectWhenMissing('/settings/indices'));
}
})
.when('/settings/indices/:id/scriptedField', {
template: require('text!plugins/settings/sections/indices/scripted_fields/index.html'),
})
.when('/settings/indices/:id/scriptedField/:field', {
template: require('text!plugins/settings/sections/indices/scripted_fields/index.html'),
});
require('modules').get('apps/settings')
.controller('scriptedFieldsEdit', function ($scope, $route, $window, Notifier, Private, kbnUrl) {
var fieldTypes = Private(require('components/index_patterns/_field_types'));
var indexPatternPath = '/settings/indices/{{ indexPattern }}?_a=(tab:scriptedFields)';
var notify = new Notifier();
var createMode = (!$route.current.params.field);
$scope.indexPattern = $route.current.locals.indexPattern;
$scope.fieldTypes = fieldTypes;
if (createMode) {
$scope.action = 'Create';
} else {
var scriptName = $route.current.params.field;
$scope.action = 'Edit';
$scope.scriptedField = _.find($scope.indexPattern.fields, {
name: scriptName,
scripted: true
});
}
$scope.goBack = function () {
kbnUrl.change(indexPatternPath, {
indexPattern: $scope.indexPattern.id
});
};
$scope.submit = function () {
var field = _.defaults($scope.scriptedField, {
type: 'number',
lang: 'expression'
});
try {
if (createMode) {
$scope.indexPattern.addScriptedField(field.name, field.script, field.type, field.lang);
} else {
$scope.indexPattern.save();
}
notify.info('Scripted field \'' + $scope.scriptedField.name + '\' successfully saved');
$scope.goBack();
} catch (e) {
notify.error(e.message);
}
};
$scope.$watch('scriptedField.name', function (name) {
checkConflict(name);
});
function checkConflict(name) {
var match = _.find($scope.indexPattern.getFields(), {
name: name
});
if (match) {
$scope.namingConflict = true;
} else {
$scope.namingConflict = false;
}
}
});
});

View file

@ -7,7 +7,7 @@ define(function (require) {
function RouteManager() {
var when = [];
var additions = [];
var defaults = [];
var otherwise;
return {
@ -17,8 +17,8 @@ define(function (require) {
},
// before attaching the routes to the routeProvider, test the RE
// against the .when() path and add/override the resolves if there is a match
addResolves: function (RE, additionalResolves) {
additions.push([RE, additionalResolves]);
defaults: function (RE, def) {
defaults.push([RE, def]);
return this;
},
otherwise: function (route) {
@ -30,10 +30,10 @@ define(function (require) {
var path = args[0];
var route = args[1] || {};
// merge in any additions
additions.forEach(function (addition) {
if (addition[0].test(path)) {
route.resolve = _.assign(route.resolve || {}, addition[1]);
// merge in any defaults
defaults.forEach(function (args) {
if (args[0].test(path)) {
_.merge(route, args[1]);
}
});
@ -55,4 +55,4 @@ define(function (require) {
}
return new RouteManager();
});
});

View file

@ -1,14 +1,12 @@
define(function (require) {
var angular = require('angular');
var _ = require('lodash');
var RouteManager = require('routes').RouteManager;
var routes; // will contain an new instance of RouteManager for each test
var sinon = require('test_utils/auto_release_sinon');
var getRouteProvider = require('./_get_route_provider');
var chainableMethods = [
{ name: 'when', args: ['', {}] },
{ name: 'otherwise', args: [{}] },
{ name: 'addResolves', args: [/regexp/, {}] }
{ name: 'defaults', args: [/regexp/, {}] }
];
describe('Custom Route Management', function () {
@ -88,9 +86,6 @@ define(function (require) {
routes.when('/nothing-set');
routes.when('/no-reload', { reloadOnSearch: false });
routes.when('/always-reload', { reloadOnSearch: true });
var exec = 0;
routes.config($rp);
expect($rp.when.callCount).to.be(3);
@ -103,4 +98,4 @@ define(function (require) {
require('specs/utils/routes/_work_queue')();
require('specs/utils/routes/_wrap_route_with_prep')();
});
});
});