Merge pull request #7372 from elastic/feature/ingest

Add Data - CSV
This commit is contained in:
Matt Bargar 2016-06-17 16:23:46 -04:00 committed by GitHub
commit 15a4fa1cdd
213 changed files with 9707 additions and 179 deletions

View file

@ -68,6 +68,7 @@
},
"dependencies": {
"@bigfunger/decompress-zip": "0.2.0-stripfix2",
"@bigfunger/jsondiffpatch": "0.1.38-webpack",
"@elastic/datemath": "2.3.0",
"@spalger/angular-bootstrap": "0.12.1",
"@spalger/filesaver": "1.1.2",
@ -76,6 +77,7 @@
"@spalger/numeral": "^2.0.0",
"@spalger/test-subj-selector": "0.2.1",
"@spalger/ui-ace": "0.2.3",
"JSONStream": "1.1.1",
"angular": "1.4.7",
"angular-bootstrap-colorpicker": "3.0.19",
"angular-elastic": "2.5.0",
@ -95,6 +97,7 @@
"clipboard": "1.5.5",
"commander": "2.8.1",
"css-loader": "0.17.0",
"csv-parse": "1.1.0",
"d3": "3.5.6",
"dragula": "3.7.0",
"elasticsearch": "10.1.2",
@ -110,6 +113,7 @@
"good-squeeze": "2.1.0",
"gridster": "0.5.6",
"hapi": "8.8.1",
"highland": "2.7.2",
"httpolyglot": "0.1.1",
"imports-loader": "0.6.4",
"jade": "1.11.0",
@ -130,6 +134,7 @@
"moment": "2.13.0",
"moment-timezone": "0.5.4",
"node-uuid": "1.4.7",
"papaparse": "4.1.2",
"raw-loader": "0.5.1",
"request": "2.61.0",
"rimraf": "2.4.3",

View file

@ -34,6 +34,9 @@ export default class BasePathProxy {
config.set('server.basePath', this.basePath);
}
const ONE_GIGABYTE = 1024 * 1024 * 1024;
config.set('server.maxPayloadBytes', ONE_GIGABYTE);
setupLogging(null, this.server, config);
setupConnection(null, this.server, config);
this.setupRoutes();

View file

@ -0,0 +1,31 @@
import expect from 'expect.js';
import {patternToIngest, ingestToPattern} from '../convert_pattern_and_ingest_name';
describe('convertPatternAndTemplateName', function () {
describe('ingestToPattern', function () {
it('should convert an index template\'s name to its matching index pattern\'s title', function () {
expect(ingestToPattern('kibana-logstash-*')).to.be('logstash-*');
});
it('should throw an error if the template name isn\'t a valid kibana namespaced name', function () {
expect(ingestToPattern).withArgs('logstash-*').to.throwException('not a valid kibana namespaced template name');
expect(ingestToPattern).withArgs('').to.throwException(/not a valid kibana namespaced template name/);
});
});
describe('patternToIngest', function () {
it('should convert an index pattern\'s title to its matching index template\'s name', function () {
expect(patternToIngest('logstash-*')).to.be('kibana-logstash-*');
});
it('should throw an error if the pattern is empty', function () {
expect(patternToIngest).withArgs('').to.throwException(/pattern must not be empty/);
});
});
});

View file

@ -4,7 +4,7 @@
// This module provides utility functions for easily converting between template and pattern names.
module.exports = {
templateToPattern: (templateName) => {
ingestToPattern: (templateName) => {
if (templateName.indexOf('kibana-') === -1) {
throw new Error('not a valid kibana namespaced template name');
}
@ -12,7 +12,7 @@ module.exports = {
return templateName.slice(templateName.indexOf('-') + 1);
},
patternToTemplate: (patternName) => {
patternToIngest: (patternName) => {
if (patternName === '') {
throw new Error('pattern must not be empty');
}

View file

@ -62,6 +62,7 @@
ng-attr-placeholder="{{index.defaultName}}"
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 2500, 'blur': 0} }"
validate-index-name
allow-wildcard
name="name"
required
type="text"
@ -167,6 +168,7 @@
</div>
<button
data-test-subj="submitCreateIndexPatternFromExistingForm"
ng-disabled="form.$invalid || index.fetchFieldsError"
ng-class="index.fetchFieldsError ? 'btn-default' : 'btn-success'"
type="submit"

View file

@ -1,6 +1,6 @@
<kbn-management-app section="kibana">
<kbn-management-indices>
<div ng-controller="managementIndicesEdit">
<div ng-controller="managementIndicesEdit" data-test-subj="editIndexPattern">
<div class="page-header">
<kbn-management-index-header
index-pattern="indexPattern"

View file

@ -8,6 +8,7 @@ import IndicesFieldTypesProvider from 'plugins/kibana/management/sections/indice
import uiRoutes from 'ui/routes';
import uiModules from 'ui/modules';
import editTemplate from 'plugins/kibana/management/sections/indices/_edit.html';
import IngestProvider from 'ui/ingest';
uiRoutes
.when('/management/kibana/indices/:indexPatternId?', {
@ -32,6 +33,7 @@ uiModules.get('apps/management')
const notify = new Notifier();
const $state = $scope.state = new AppState();
const refreshKibanaIndex = Private(RefreshKibanaIndex);
const ingest = Private(IngestProvider);
$scope.kbnUrl = Private(UrlProvider);
$scope.indexPattern = $route.current.locals.indexPattern;
@ -68,8 +70,8 @@ uiModules.get('apps/management')
}
}
courier.indexPatterns.delete($scope.indexPattern)
.then(refreshKibanaIndex)
ingest.delete($scope.indexPattern.id)
.then($scope.indexPattern.destroy.bind($scope.indexPattern))
.then(function () {
$location.url('/management/data/index');
})

View file

@ -0,0 +1,84 @@
<h2><em>Follow these instructions to install Filebeat.</em>
Now that you've got a fresh pipeline and index pattern, let's throw some data at it!
</h2>
<div class="install-filebeat">
<ol>
<li>
<span>
<strong>Install Filebeat</strong> on all servers on which you want to tail logs &nbsp;
<a target="_blank" ng-href="{{installStep.docLinks.installation}}">
<i aria-hidden="true" class="fa fa-info-circle"></i> instructions
</a>
</span>
</li>
<li>
<span>
<strong>Point Filebeat</strong> at the log files you want to tail &nbsp;
<a target="_blank" ng-href="{{installStep.docLinks.configuration}}">
<i aria-hidden="true" class="fa fa-info-circle"></i> instructions
</a>
</span>
</li>
<li ng-if="installStep.results.pipeline.processors.length">
<span>
<strong>Configure Filebeat</strong> to send data through your new Elasticsearch pipeline &nbsp;
<a target="_blank" ng-href="{{installStep.docLinks.elasticsearchOutput}}">
<i aria-hidden="true" class="fa fa-info-circle"></i> instructions
</a><br/>
At minimum you'll need to configure Filebeat's Elasticsearch output with a hostname, an index name, and a
<a target="_blank"
ng-href="{{installStep.docLinks.elasticsearchOutputAnchorParameters}}">
<i aria-hidden="true" class="fa fa-info-circle"></i> paramaters
</a> block. Your config should end up looking something like this:<br/>
<pre>
output:
elasticsearch:
hosts: ["your-elasticsearch-host"]
index: "your-base-index-name"
parameters:
pipeline: "{{installStep.pipelineId}}"</pre>
<em>NOTE</em>: The Filebeat config takes a base index name and automatically rotates the target index by appending "-{date}"
to the end, so if your pattern was "filebeat-*" you would make the index name "filebeat" in filebeat.yml.<br />
</span>
</li>
<li ng-if="!installStep.results.pipeline.processors.length">
<span>
<strong>Configure Filebeat</strong> to send data to Elasticsearch &nbsp;
<a target="_blank" ng-href="{{installStep.docLinks.elasticsearchOutput}}">
<i aria-hidden="true" class="fa fa-info-circle"></i> instructions
</a><br/>
At minimum you'll need to configure Filebeat's Elasticsearch output with a hostname and an index name.
Your config should end up looking something like this:<br />
<pre>
output:
elasticsearch:
hosts: ["your-elasticsearch-host"]
index: "your-base-index-name"</pre>
<em>NOTE</em>: The Filebeat config takes a base index name and automatically rotates the target index by appending "-{date}"
to the end, so if your pattern was "filebeat-*" you would make the index name "filebeat" in filebeat.yml.<br />
</span>
</li>
<li>
<span>
<strong>Run Filebeat</strong> on each server &nbsp;
<a target="_blank" ng-href="{{installStep.docLinks.startup}}">
<i aria-hidden="true" class="fa fa-info-circle"></i> instructions
</a>
</span>
</li>
<li>
<span>
<strong>Verify your filebeat installation below.</strong> We'll poll your new index pattern for documents and let you know when
they show up. If you'd like to skip this step, simply click Done now.
</span>
</li>
</ol>
</div>
<pattern-checker pattern="installStep.results.indexPattern.id"/>

View file

@ -0,0 +1,23 @@
import modules from 'ui/modules';
import template from './install_filebeat_step.html';
import 'ui/pattern_checker';
import { patternToIngest } from '../../../../../../common/lib/convert_pattern_and_ingest_name';
import { filebeat as docLinks } from '../../../../../../../../ui/public/documentation_links/documentation_links';
import './styles/_add_data_install_filebeat_step.less';
modules.get('apps/management')
.directive('installFilebeatStep', function () {
return {
template: template,
scope: {
results: '='
},
bindToController: true,
controllerAs: 'installStep',
controller: function ($scope) {
this.pipelineId = patternToIngest(this.results.indexPattern.id);
this.docLinks = docLinks;
}
};
});

View file

@ -0,0 +1,22 @@
install-filebeat-step {
.install-filebeat {
> ol {
padding-left: 1em;
> li {
padding: 4px 0;
font-weight: bold;
> span {
font-weight: normal;
> pre {
margin: 7px 0;
}
}
}
}
}
}

View file

@ -0,0 +1,63 @@
<file-upload ng-if="!wizard.file" on-locate="wizard.file = file" upload-selector="button.upload">
<h2><em>Pick a CSV file to get started.</em>
Please follow the instructions below.
</h2>
<div class="upload-wizard-file-upload-container">
<div class="upload-instructions">Drop your file here</div>
<div class="upload-instructions-separator">or</div>
<button class="btn btn-primary btn-lg controls upload" ng-click>
Select File
</button>
<div>Maximum upload file size: 1 GB</div>
</div>
</file-upload>
<div class="upload-wizard-file-preview-container" ng-if="wizard.file">
<h2><em>Review the sample below.</em>
Click next if it looks like we parsed your file correctly.
</h2>
<div ng-if="!!wizard.formattedErrors.length" class="alert alert-danger parse-error">
<ul>
<li ng-repeat="error in wizard.formattedErrors track by $index">{{ error }}</li>
</ul>
</div>
<div ng-if="!!wizard.formattedWarnings.length" class="alert alert-warning">
<ul>
<li ng-repeat="warning in wizard.formattedWarnings track by $index">{{ warning }}</li>
</ul>
</div>
<div class="advanced-options form-inline">
<span class="form-group">
<label>Delimiter</label>
<select ng-model="wizard.parseOptions.delimiter"
ng-options="option.value as option.label for option in wizard.delimiterOptions"
class="form-control">
</select>
</span>
<span class="form-group">
<label>Filename:</label>
{{ wizard.file.name }}
</span>
</div>
<div class="preview">
<table class="table table-condensed">
<thead>
<tr>
<th ng-repeat="col in wizard.columns track by $index">
<span title="{{ col }}">{{ col | limitTo:12 }}{{ col.length > 12 ? '...' : '' }}</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in wizard.rows">
<td ng-repeat="cell in row track by $index">{{ cell }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View file

@ -0,0 +1,139 @@
import _ from 'lodash';
import Papa from 'papaparse';
import modules from 'ui/modules';
import template from './parse_csv_step.html';
import './styles/_add_data_parse_csv_step.less';
modules.get('apps/management')
.directive('parseCsvStep', function () {
return {
restrict: 'E',
template: template,
scope: {
file: '=',
parseOptions: '=',
samples: '='
},
bindToController: true,
controllerAs: 'wizard',
controller: function ($scope, debounce) {
const maxSampleRows = 10;
const maxSampleColumns = 20;
this.delimiterOptions = [
{
label: 'comma',
value: ','
},
{
label: 'tab',
value: '\t'
},
{
label: 'space',
value: ' '
},
{
label: 'semicolon',
value: ';'
},
{
label: 'pipe',
value: '|'
}
];
this.parse = debounce(() => {
if (!this.file) return;
let row = 1;
let rows = [];
let data = [];
delete this.rows;
delete this.columns;
this.formattedErrors = [];
this.formattedWarnings = [];
const config = _.assign(
{
header: true,
dynamicTyping: true,
step: (results, parser) => {
if (row > maxSampleRows) {
parser.abort();
// The complete callback isn't automatically called if parsing is manually aborted
config.complete();
return;
}
if (row === 1) {
// Collect general information on the first pass
if (results.meta.fields.length > _.uniq(results.meta.fields).length) {
this.formattedErrors.push('Column names must be unique');
}
let hasEmptyHeader = false;
_.forEach(results.meta.fields, (field) => {
if (_.isEmpty(field)) {
hasEmptyHeader = true;
}
});
if (hasEmptyHeader) {
this.formattedErrors.push('Column names must not be blank');
}
if (results.meta.fields.length > maxSampleColumns) {
this.formattedWarnings.push(`Preview truncated to ${maxSampleColumns} columns`);
}
this.columns = results.meta.fields.slice(0, maxSampleColumns);
this.parseOptions = _.defaults({}, this.parseOptions, {delimiter: results.meta.delimiter});
}
this.formattedErrors = this.formattedErrors.concat(_.map(results.errors, (error) => {
return `${error.type} at line ${row + 1} - ${error.message}`;
}));
data = data.concat(results.data);
rows = rows.concat(_.map(results.data, (row) => {
return _.map(this.columns, (columnName) => {
return row[columnName];
});
}));
++row;
},
complete: () => {
$scope.$apply(() => {
this.rows = rows;
if (_.isUndefined(this.formattedErrors) || _.isEmpty(this.formattedErrors)) {
this.samples = data;
}
else {
delete this.samples;
}
});
}
},
this.parseOptions
);
Papa.parse(this.file, config);
}, 100);
$scope.$watch('wizard.parseOptions', (newValue, oldValue) => {
// Delimiter is auto-detected in the first run of the parse function, so we don't want to
// re-parse just because it's being initialized.
if (!_.isUndefined(oldValue)) {
this.parse();
}
}, true);
$scope.$watch('wizard.file', () => {
this.parse();
});
}
};
});

View file

@ -0,0 +1,64 @@
@import (reference) "../../../styles/_add_data_wizard";
.upload-wizard-file-upload-container {
min-height: 300px;
display: flex;
flex-direction: column;
justify-content: center;
background-color: @settings-add-data-wizard-form-control-bg;
border: @settings-add-data-wizard-parse-csv-container-border 1px dashed;
text-align: center;
.upload-instructions {
font-size: 2em;
}
.upload-instructions-separator {
margin: 15px 0;
}
button {
width: inherit;
}
button.upload {
align-self: center;
margin-bottom: 15px;
}
}
.upload-wizard-file-preview-container {
.preview {
overflow: auto;
max-height: 500px;
border: @settings-add-data-wizard-parse-csv-container-border 1px solid;
table {
margin-bottom: 0;
.table-striped()
}
}
.parse-error {
margin-top: 2em;
}
.advanced-options {
display: flex;
align-items: center;
.form-group {
display: flex;
align-items: center;
padding-right: 15px;
label {
padding-right: 8px;
margin-bottom: 0;
}
}
padding-bottom: 10px;
}
}

View file

@ -0,0 +1,11 @@
<h2><em>Provide some sample logs.</em>
Paste in one or more lines from the file you intend to tail. We'll use these samples in the following steps to help
you build an ingest pipeline and configure a Kibana index pattern. Log lines can be raw strings or
formatted as JSON. If your logs are raw strings but you intend to use
<a target="_window" ng-href="{{pasteStep.docLinks.exportedFields}}">Filebeat's metadata</a>,
you'll want to paste the JSON as it will come out of Filebeat.
</h2>
<div class="paste-samples form-group">
<textarea class="form-control" ng-model="pasteStep.rawSamples" placeholder="Paste your sample log lines here, separated by a newline"></textarea>
</div>

View file

@ -0,0 +1,41 @@
import modules from 'ui/modules';
import template from './paste_samples_step.html';
import { filebeat as docLinks } from '../../../../../../../../ui/public/documentation_links/documentation_links';
import _ from 'lodash';
import './styles/_add_data_paste_samples_step.less';
modules.get('apps/management')
.directive('pasteSamplesStep', function () {
return {
template: template,
scope: {
samples: '=',
rawSamples: '='
},
bindToController: true,
controllerAs: 'pasteStep',
controller: function ($scope) {
this.docLinks = docLinks;
if (_.isUndefined(this.rawSamples)) {
this.rawSamples = '';
}
$scope.$watch('pasteStep.rawSamples', (newValue) => {
const splitRawSamples = newValue.split('\n');
try {
this.samples = _.map(splitRawSamples, (sample) => {
return JSON.parse(sample);
});
}
catch (error) {
this.samples = _.map(splitRawSamples, (sample) => {
return {message: sample};
});
}
});
}
};
});

View file

@ -0,0 +1,6 @@
.paste-samples {
textarea {
width: 100%;
height: 250px;
}
}

View file

@ -0,0 +1,64 @@
import forEachField from '../lib/for_each_field';
import sinon from 'auto-release-sinon';
import expect from 'expect.js';
describe('forEachField', function () {
let testDoc;
beforeEach(function () {
testDoc = {
foo: [
{bar: [{'baz': 1}]},
{bat: 'boo'}
]
};
});
it('should require a plain object argument', function () {
expect(forEachField).withArgs([], () => {}).to.throwException(/first argument must be a plain object/);
});
it('should not invoke iteratee if collection is null or empty', function () {
const iteratee = sinon.spy();
forEachField({}, iteratee);
expect(iteratee.called).to.not.be.ok();
});
it('should call iteratee for each item in an array field, but not for the array itself', function () {
const iteratee = sinon.spy();
forEachField({foo: [1, 2, 3]}, iteratee);
expect(iteratee.callCount).to.be(3);
expect(iteratee.calledWith(1, 'foo')).to.be.ok();
expect(iteratee.calledWith(2, 'foo')).to.be.ok();
expect(iteratee.calledWith(3, 'foo')).to.be.ok();
});
it('should call iteratee for flattened inner object properties, as well as the object itself', function () {
const iteratee = sinon.spy();
forEachField(testDoc, iteratee);
expect(iteratee.callCount).to.be(5);
expect(iteratee.calledWith(testDoc.foo[0], 'foo')).to.be.ok();
expect(iteratee.calledWith(testDoc.foo[1], 'foo')).to.be.ok();
expect(iteratee.calledWith(testDoc.foo[0].bar[0], 'foo.bar')).to.be.ok();
expect(iteratee.calledWith(1, 'foo.bar.baz')).to.be.ok();
expect(iteratee.calledWith('boo', 'foo.bat')).to.be.ok();
});
it('should detect geo_point fields and should not invoke iteratee for its lat and lon sub properties', function () {
const iteratee = sinon.spy();
const geo = {lat: 38.6631, lon: -90.5771};
forEachField({ geo }, iteratee);
expect(iteratee.callCount).to.be(1);
expect(iteratee.calledWith(geo, 'geo')).to.be.ok();
});
});

View file

@ -0,0 +1,21 @@
import isGeoPointObject from '../lib/is_geo_point_object';
import expect from 'expect.js';
describe('isGeoPointObject', function () {
it('should return true if an object has lat and lon properties', function () {
expect(isGeoPointObject({lat: 38.6631, lon: -90.5771})).to.be(true);
});
it('should return false if the value is not an object', function () {
expect(isGeoPointObject('foo')).to.be(false);
expect(isGeoPointObject(1)).to.be(false);
expect(isGeoPointObject(true)).to.be(false);
expect(isGeoPointObject(null)).to.be(false);
});
it('should return false if the value is an object without lat an lon properties', function () {
expect(isGeoPointObject({foo: 'bar'})).to.be(false);
});
});

View file

@ -0,0 +1,91 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
describe('pattern review directive', function () {
let $rootScope;
let $compile;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector, Private) {
$compile = $injector.get('$compile');
$rootScope = $injector.get('$rootScope');
}));
describe('handling geopoints', function () {
it('should detect geo_point fields when they\'re expressed as an object', function () {
const scope = $rootScope.$new();
scope.sampleDoc = {
geoip: {
location: {
lat: 38.6631,
lon: -90.5771
}
}
};
$compile('<pattern-review-step sample-doc="sampleDoc" index-pattern="indexPattern"></pattern-review-step>')(scope);
scope.$digest();
expect(scope).to.have.property('indexPattern');
expect(scope.indexPattern.fields[0].type).to.be('geo_point');
});
it('should not count the lat and lon properties as their own fields', function () {
const scope = $rootScope.$new();
scope.sampleDoc = {
geoip: {
location: {
lat: 38.6631,
lon: -90.5771
}
}
};
$compile('<pattern-review-step sample-doc="sampleDoc" index-pattern="indexPattern"></pattern-review-step>')(scope);
scope.$digest();
expect(scope).to.have.property('indexPattern');
expect(scope.indexPattern.fields[0].type).to.be('geo_point');
expect(scope.indexPattern.fields.length).to.be(1);
});
});
describe('detecting date fields', function () {
it('should detect sample strings in ISO 8601 format as date fields', function () {
const scope = $rootScope.$new();
scope.sampleDoc = {
isodate: '2004-03-08T00:05:49.000Z'
};
$compile('<pattern-review-step sample-doc="sampleDoc" index-pattern="indexPattern"></pattern-review-step>')(scope);
scope.$digest();
expect(scope).to.have.property('indexPattern');
expect(scope.indexPattern.fields[0].type).to.be('date');
});
});
describe('conflicting array values', function () {
it('should detect heterogeneous arrays and flag them with an error message', function () {
const scope = $rootScope.$new();
scope.sampleDoc = {
badarray: ['foo', 42]
};
const element = $compile('<pattern-review-step sample-doc="sampleDoc" index-pattern="indexPattern"></pattern-review-step>')(scope);
const controller = element.controller('patternReviewStep');
scope.$digest();
expect(controller).to.have.property('errors');
// error message should mentioned the conflicting field
expect(controller.errors[0]).to.contain('badarray');
});
});
});

View file

@ -0,0 +1,58 @@
import _ from 'lodash';
import isGeoPointObject from './is_geo_point_object';
// This function recursively traverses an object, visiting each node that elasticsearch would index as a field.
// Iteratee is invoked with two arguments: (value, fieldName). fieldName is the name of the field as elasticsearch
// would see it. For example:
//
// const testDoc = {
// foo: [
// {bar: [{'baz': 1}]},
// {bat: 'boo'}
// ],
// geo: {
// lat: 38.6631,
// lon: -90.5771
// }
// };
//
// forEachField(testDoc, function(value, fieldName) { ... });
//
// The iteratee would be invoked six times, with the following parameters:
// 1. fieldName = 'foo' value = {bar: [{'baz': 1}]}
// 2. fieldName = 'foo' value = {bat: 'boo'}
// 3. fieldName = 'foo.bar' value = {'baz': 1}
// 4. fieldName = 'foo.bar.baz' value = 1
// 5. fieldName = 'foo.bat' value = 'boo'
// 6. fieldName = 'geo' value = {lat: 38.6631, lon: -90.5771}
//
// forEachField handles arrays, objects, and geo_points as elasticsearch would. It does not currently handle nested
// type fields.
function forEachFieldAux(value, iteratee, fieldName) {
if (!_.isObject(value) || isGeoPointObject(value)) {
iteratee(value, fieldName);
}
else if (_.isPlainObject(value)) {
if (!_.isEmpty(fieldName)) {
iteratee(value, fieldName);
fieldName += '.';
}
_.forEach(value, (subValue, key) => {
forEachFieldAux(subValue, iteratee, fieldName + key);
});
}
else if (_.isArray(value)) {
_.forEach(value, (subValue) => {
forEachFieldAux(subValue, iteratee, fieldName);
});
}
}
export default function forEachField(object, iteratee) {
if (!_.isPlainObject(object)) {
throw new Error('first argument must be a plain object');
}
forEachFieldAux(object, iteratee, '');
}

View file

@ -0,0 +1,15 @@
import _ from 'lodash';
export default function isGeoPointObject(object) {
let retVal = false;
if (_.isPlainObject(object)) {
const keys = _.keys(object);
if (keys.length === 2 && _.contains(keys, 'lat') && _.contains(keys, 'lon')) {
retVal = true;
}
}
return retVal;
}

View file

@ -0,0 +1,50 @@
<h2><em>Review the index pattern.</em>
Here we'll define how and where to store your parsed events. We've made some intelligent guesses for you, but most
fields can be changed if we got it wrong!
</h2>
<form name="reviewStep.form">
<div class="pattern-review form-inline">
<div ng-show="reviewStep.errors.length" class="alert alert-danger">
<div ng-repeat="error in reviewStep.errors">{{ error }}</div>
</div>
<div class="alert alert-danger"
ng-show="reviewStep.form.pattern.$dirty && reviewStep.form.pattern.$error.lowercase">
Index names must be all lowercase
</div>
<div class="alert alert-danger"
ng-show="reviewStep.form.pattern.$dirty && reviewStep.form.pattern.$error.indexNameInput">
An index name must not be empty and cannot contain whitespace or any of the following characters: ", *, \, <, |, ,, >, /, ?
</div>
<label>{{ reviewStep.patternInput.label }}</label>
<span id="pattern-help" class="help-block">{{ reviewStep.patternInput.helpText }}</span>
<input name="pattern" ng-model="reviewStep.indexPattern.id"
class="pattern-input form-control"
novalidate
required
validate-index-name
validate-lowercase
placeholder="{{reviewStep.patternInput.placeholder}}"
aria-describedby="pattern-help"/>
<label>
<input ng-model="reviewStep.isTimeBased" type="checkbox"/>
time based
</label>
<label ng-if="reviewStep.isTimeBased" class="time-field-input">
Time Field
<select ng-model="reviewStep.indexPattern.timeFieldName" name="time_field_name" class="form-control">
<option ng-repeat="field in reviewStep.dateFields" value="{{field}}">
{{field}}
</option>
</select>
</label>
</div>
<paginated-table
class="pattern-review-field-table"
columns="reviewStep.columns"
rows="reviewStep.rows"
per-page="10">
</paginated-table>
</form>

View file

@ -0,0 +1,132 @@
import modules from 'ui/modules';
import template from './pattern_review_step.html';
import _ from 'lodash';
import editFieldTypeHTML from '../../partials/_edit_field_type.html';
import isGeoPointObject from './lib/is_geo_point_object';
import forEachField from './lib/for_each_field';
import './styles/_add_data_pattern_review_step.less';
import moment from 'moment';
import '../../../../../../../../ui/public/directives/validate_lowercase';
function pickDefaultTimeFieldName(dateFields) {
if (_.isEmpty(dateFields)) {
return undefined;
}
return _.includes(dateFields, '@timestamp') ? '@timestamp' : dateFields[0];
}
function findFieldsByType(indexPatternFields, type) {
return _.map(_.filter(indexPatternFields, {type}), 'name');
}
modules.get('apps/management')
.directive('patternReviewStep', function () {
return {
template: template,
scope: {
indexPattern: '=',
pipeline: '=',
sampleDoc: '=',
defaultIndexInput: '='
},
controllerAs: 'reviewStep',
bindToController: true,
controller: function ($scope, Private) {
this.errors = [];
const sampleFields = {};
this.patternInput = {
label: 'Index name',
helpText: 'The name of the Elasticsearch index you want to create for your data.',
defaultValue: '',
placeholder: 'Name'
};
if (this.defaultIndexInput) {
this.patternInput.defaultValue = this.defaultIndexInput;
}
if (_.isUndefined(this.indexPattern)) {
this.indexPattern = {};
}
forEachField(this.sampleDoc, (value, fieldName) => {
let type = typeof value;
if (isGeoPointObject(value)) {
type = 'geo_point';
}
if (type === 'string' && moment(value, moment.ISO_8601).isValid()) {
type = 'date';
}
if (value === null) {
type = 'string';
}
if (!_.isUndefined(sampleFields[fieldName]) && (sampleFields[fieldName].type !== type)) {
this.errors.push(`Error in field ${fieldName} - conflicting types '${sampleFields[fieldName].type}' and '${type}'`);
}
else {
sampleFields[fieldName] = {type, value};
}
});
_.defaults(this.indexPattern, {
id: this.patternInput.defaultValue,
title: 'filebeat-*',
fields: _(sampleFields)
.map((field, fieldName) => {
return {name: fieldName, type: field.type};
})
.reject({type: 'object'})
.value()
});
$scope.$watch('reviewStep.indexPattern.id', (value) => {
this.indexPattern.title = value;
});
$scope.$watch('reviewStep.isTimeBased', (value) => {
if (value) {
this.indexPattern.timeFieldName = pickDefaultTimeFieldName(this.dateFields);
}
else {
delete this.indexPattern.timeFieldName;
}
});
$scope.$watch('reviewStep.indexPattern.fields', (fields) => {
this.dateFields = findFieldsByType(fields, 'date');
}, true);
this.dateFields = findFieldsByType(this.indexPattern.fields, 'date');
this.isTimeBased = !_.isEmpty(this.dateFields);
const buildRows = () => {
this.rows = _.map(this.indexPattern.fields, (field) => {
const {type: detectedType, value: sampleValue} = sampleFields[field.name];
return [
_.escape(field.name),
{
markup: editFieldTypeHTML,
scope: _.assign($scope.$new(), {field: field, detectedType: detectedType, buildRows: buildRows}),
value: field.type
},
typeof sampleValue === 'object' ? _.escape(JSON.stringify(sampleValue)) : _.escape(sampleValue)
];
});
};
this.columns = [
{title: 'Field'},
{title: 'Type'},
{title: 'Example', sortable: false}
];
buildRows();
}
};
});

View file

@ -0,0 +1,71 @@
@import (reference) "../../../styles/_add_data_wizard";
pattern-review-step {
margin-bottom: 14px;
.pattern-review {
margin-bottom: 15px;
label {
margin-bottom: 0;
}
.time-field-input {
padding-left: 14px;
margin-bottom: 0;
}
.pattern-input {
width: 300px;
margin-right: 7px;
}
> .help-block {
margin-top: 0;
}
}
paginated-table.pattern-review-field-table {
table {
border-bottom: 3px solid @settings-filebeat-wizard-panel-bg;
tr {
.form-group;
}
th {
border-bottom: 0;
padding-top: 10px;
padding-bottom: 10px;
background-color: @settings-filebeat-wizard-panel-bg;
font-weight: normal;
}
td {
border-top: 3px solid @settings-filebeat-wizard-panel-bg;
vertical-align: middle;
padding-right: 14px;
}
select {
.form-control;
.wizard-container.form-control;
min-width: 105px;
}
}
paginate-controls {
position: relative;
ul > li > a {
background-color: @settings-filebeat-wizard-panel-bg;
}
form.pagination-size {
position: absolute;
right: 0;
}
}
}
}

View file

@ -0,0 +1,50 @@
import uiModules from 'ui/modules';
import jsondiffpatch from '@bigfunger/jsondiffpatch';
import '../styles/_output_preview.less';
import outputPreviewTemplate from '../views/output_preview.html';
const htmlFormat = jsondiffpatch.formatters.html.format;
const app = uiModules.get('kibana');
app.directive('outputPreview', function () {
return {
restrict: 'E',
template: outputPreviewTemplate,
scope: {
oldObject: '=',
newObject: '=',
error: '='
},
link: function ($scope, $el) {
const div = $el.find('.visual')[0];
$scope.diffpatch = jsondiffpatch.create({
arrays: {
detectMove: false
},
textDiff: {
minLength: 120
}
});
$scope.updateUi = function () {
let left = $scope.oldObject;
let right = $scope.newObject;
let delta = $scope.diffpatch.diff(left, right);
if (!delta || $scope.error) delta = {};
div.innerHTML = htmlFormat(delta, left);
};
},
controller: function ($scope, debounce) {
$scope.collapsed = false;
const updateOutput = debounce(function () {
$scope.updateUi();
}, 200);
$scope.$watch('oldObject', updateOutput);
$scope.$watch('newObject', updateOutput);
}
};
});

View file

@ -0,0 +1,20 @@
import uiModules from 'ui/modules';
import '../styles/_pipeline_output.less';
import pipelineOutputTemplate from '../views/pipeline_output.html';
const app = uiModules.get('kibana');
app.directive('pipelineOutput', function () {
return {
restrict: 'E',
template: pipelineOutputTemplate,
scope: {
pipeline: '=',
samples: '=',
sample: '='
},
controller: function ($scope) {
$scope.collapsed = true;
}
};
});

View file

@ -0,0 +1,94 @@
import uiModules from 'ui/modules';
import _ from 'lodash';
import Pipeline from '../lib/pipeline';
import angular from 'angular';
import * as ProcessorTypes from '../processors/view_models';
import IngestProvider from 'ui/ingest';
import '../styles/_pipeline_setup.less';
import './pipeline_output';
import './source_data';
import './processor_ui_container';
import '../processors';
import pipelineSetupTemplate from '../views/pipeline_setup.html';
const app = uiModules.get('kibana');
function buildProcessorTypeList(enabledProcessorTypeIds) {
return _(ProcessorTypes)
.map(Type => {
const instance = new Type();
return {
typeId: instance.typeId,
title: instance.title,
Type
};
})
.compact()
.filter((processorType) => enabledProcessorTypeIds.includes(processorType.typeId))
.sortBy('title')
.value();
}
app.directive('pipelineSetup', function () {
return {
restrict: 'E',
template: pipelineSetupTemplate,
scope: {
samples: '=',
pipeline: '='
},
controller: function ($scope, debounce, Private, Notifier) {
const ingest = Private(IngestProvider);
const notify = new Notifier({ location: `Ingest Pipeline Setup` });
$scope.sample = {};
//determines which processors are available on the cluster
ingest.getProcessors()
.then((enabledProcessorTypeIds) => {
$scope.processorTypes = buildProcessorTypeList(enabledProcessorTypeIds);
})
.catch(notify.error);
const pipeline = new Pipeline();
// Loads pre-existing pipeline which will exist if the user returns from
// a later step in the wizard
if ($scope.pipeline) {
pipeline.load($scope.pipeline);
$scope.sample = $scope.pipeline.input;
}
$scope.pipeline = pipeline;
//initiates the simulate call if the pipeline is dirty
const simulatePipeline = debounce((event, message) => {
if (pipeline.processors.length === 0) {
pipeline.updateOutput();
return;
}
return ingest.simulate(pipeline.model)
.then((results) => { pipeline.applySimulateResults(results); })
.catch(notify.error);
}, 200);
$scope.$watchCollection('pipeline.processors', (newVal, oldVal) => {
pipeline.updateParents();
});
$scope.$watch('sample', (newVal) => {
pipeline.input = $scope.sample;
pipeline.updateParents();
});
$scope.$watch('processorType', (newVal) => {
if (!newVal) return;
pipeline.add(newVal.Type);
$scope.processorType = '';
});
$scope.$watch('pipeline.dirty', simulatePipeline);
$scope.expandContext = 1;
}
};
});

View file

@ -0,0 +1,34 @@
import uiModules from 'ui/modules';
import _ from 'lodash';
import '../styles/_processor_ui_container.less';
import './output_preview';
import './processor_ui_container_header';
import template from '../views/processor_ui_container.html';
const app = uiModules.get('kibana');
app.directive('processorUiContainer', function ($compile) {
return {
restrict: 'E',
scope: {
pipeline: '=',
processor: '='
},
template: template,
link: function ($scope, $el) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
const $container = $el.find('.processor-ui-content');
const typeId = processor.typeId;
const newScope = $scope.$new();
newScope.pipeline = pipeline;
newScope.processor = processor;
const template = `<processor-ui-${typeId}></processor-ui-${typeId}>`;
const $innerEl = $compile(template)(newScope);
$innerEl.appendTo($container);
}
};
});

View file

@ -0,0 +1,17 @@
import uiModules from 'ui/modules';
import '../styles/_processor_ui_container_header.less';
import processorUiContainerHeaderTemplate from '../views/processor_ui_container_header.html';
const app = uiModules.get('kibana');
app.directive('processorUiContainerHeader', function () {
return {
restrict: 'E',
scope: {
processor: '=',
field: '=',
pipeline: '='
},
template: processorUiContainerHeaderTemplate
};
});

View file

@ -0,0 +1,45 @@
import uiModules from 'ui/modules';
import angular from 'angular';
import '../styles/_source_data.less';
import sourceDataTemplate from '../views/source_data.html';
const app = uiModules.get('kibana');
app.directive('sourceData', function () {
return {
restrict: 'E',
scope: {
samples: '=',
sample: '=',
disabled: '='
},
template: sourceDataTemplate,
controller: function ($scope) {
const samples = $scope.samples;
if (samples.length > 0) {
$scope.selectedSample = samples[0];
}
$scope.$watch('selectedSample', (newValue) => {
//the added complexity of this directive is to strip out the properties
//that angular adds to array objects that are bound via ng-options
$scope.sample = angular.copy(newValue);
});
$scope.previousLine = function () {
let currentIndex = samples.indexOf($scope.selectedSample);
if (currentIndex <= 0) currentIndex = samples.length;
$scope.selectedSample = samples[currentIndex - 1];
};
$scope.nextLine = function () {
let currentIndex = samples.indexOf($scope.selectedSample);
if (currentIndex >= samples.length - 1) currentIndex = -1;
$scope.selectedSample = samples[currentIndex + 1];
};
}
};
});

View file

@ -0,0 +1 @@
import './directives/pipeline_setup';

View file

@ -0,0 +1,74 @@
import expect from 'expect.js';
import sinon from 'sinon';
import createMultiSelectModel from '../create_multi_select_model';
describe('createMultiSelectModel', function () {
it('should throw an error if the first argument is not an array', () => {
expect(createMultiSelectModel).withArgs('foo', []).to.throwError();
expect(createMultiSelectModel).withArgs(1234, []).to.throwError();
expect(createMultiSelectModel).withArgs(undefined, []).to.throwError();
expect(createMultiSelectModel).withArgs(null, []).to.throwError();
expect(createMultiSelectModel).withArgs([], []).to.not.throwError();
});
it('should throw an error if the second argument is not an array', () => {
expect(createMultiSelectModel).withArgs([], 'foo').to.throwError();
expect(createMultiSelectModel).withArgs([], 1234).to.throwError();
expect(createMultiSelectModel).withArgs([], undefined).to.throwError();
expect(createMultiSelectModel).withArgs([], null).to.throwError();
expect(createMultiSelectModel).withArgs([], []).to.not.throwError();
});
it('should output an array with an item for each passed in', () => {
const items = [ 'foo', 'bar', 'baz' ];
const expected = [
{ title: 'foo', selected: false },
{ title: 'bar', selected: false },
{ title: 'baz', selected: false }
];
const actual = createMultiSelectModel(items, []);
expect(actual).to.eql(expected);
});
it('should set the selected property in the output', () => {
const items = [ 'foo', 'bar', 'baz' ];
const selectedItems = [ 'bar', 'baz' ];
const expected = [
{ title: 'foo', selected: false },
{ title: 'bar', selected: true },
{ title: 'baz', selected: true }
];
const actual = createMultiSelectModel(items, selectedItems);
expect(actual).to.eql(expected);
});
it('should trim values when comparing for selected', () => {
const items = [ 'foo', 'bar', 'baz' ];
const selectedItems = [ ' bar ', ' baz ' ];
const expected = [
{ title: 'foo', selected: false },
{ title: 'bar', selected: true },
{ title: 'baz', selected: true }
];
const actual = createMultiSelectModel(items, selectedItems);
expect(actual).to.eql(expected);
});
it('should be case insensitive when comparing for selected', () => {
const items = [ 'foo', 'bar', 'baz' ];
const selectedItems = [ ' Bar ', ' BAZ ' ];
const expected = [
{ title: 'foo', selected: false },
{ title: 'bar', selected: true },
{ title: 'baz', selected: true }
];
const actual = createMultiSelectModel(items, selectedItems);
expect(actual).to.eql(expected);
});
});

View file

@ -0,0 +1,86 @@
import expect from 'expect.js';
import sinon from 'sinon';
import keysDeep from '../keys_deep';
describe('keys deep', function () {
it('should list first level properties', function () {
let object = {
property1: 'value1',
property2: 'value2'
};
let expected = [
'property1',
'property2'
];
const keys = keysDeep(object);
expect(keys).to.eql(expected);
});
it('should list nested properties', function () {
let object = {
property1: 'value1',
property2: 'value2',
property3: {
subProperty1: 'value1.1'
}
};
let expected = [
'property1',
'property2',
'property3.subProperty1',
'property3'
];
const keys = keysDeep(object);
expect(keys).to.eql(expected);
});
it('should recursivly list nested properties', function () {
let object = {
property1: 'value1',
property2: 'value2',
property3: {
subProperty1: 'value1.1',
subProperty2: {
prop1: 'value1.2.1',
prop2: 'value2.2.2'
},
subProperty3: 'value1.3'
}
};
let expected = [
'property1',
'property2',
'property3.subProperty1',
'property3.subProperty2.prop1',
'property3.subProperty2.prop2',
'property3.subProperty2',
'property3.subProperty3',
'property3'
];
const keys = keysDeep(object);
expect(keys).to.eql(expected);
});
it('should list array properties, but not contents', function () {
let object = {
property1: 'value1',
property2: [ 'item1', 'item2' ]
};
let expected = [
'property1',
'property2'
];
const keys = keysDeep(object);
expect(keys).to.eql(expected);
});
});

View file

@ -0,0 +1,480 @@
import _ from 'lodash';
import expect from 'expect.js';
import sinon from 'sinon';
import Pipeline from '../pipeline';
import * as processorTypes from '../../processors/view_models';
describe('processor pipeline', function () {
function getProcessorIds(pipeline) {
return pipeline.processors.map(p => p.processorId);
}
describe('model', function () {
it('should only contain the clean data properties', function () {
const pipeline = new Pipeline();
const actual = pipeline.model;
const expectedKeys = [ 'input', 'processors' ];
expect(_.keys(actual)).to.eql(expectedKeys);
});
it('should access the model property of each processor', function () {
const pipeline = new Pipeline();
pipeline.input = { foo: 'bar' };
pipeline.add(processorTypes.Set);
const actual = pipeline.model;
const expected = {
input: pipeline.input,
processors: [ pipeline.processors[0].model ]
};
expect(actual).to.eql(expected);
});
});
describe('load', function () {
it('should remove existing processors from the pipeline', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const oldProcessors = [ pipeline.processors[0], pipeline.processors[1], pipeline.processors[2] ];
const newPipeline = new Pipeline();
newPipeline.add(processorTypes.Set);
newPipeline.add(processorTypes.Set);
newPipeline.add(processorTypes.Set);
pipeline.load(newPipeline);
expect(_.find(pipeline.processors, oldProcessors[0])).to.be(undefined);
expect(_.find(pipeline.processors, oldProcessors[1])).to.be(undefined);
expect(_.find(pipeline.processors, oldProcessors[2])).to.be(undefined);
});
it('should call addExisting for each of the imported processors', function () {
const pipeline = new Pipeline();
sinon.stub(pipeline, 'addExisting');
const newPipeline = new Pipeline();
newPipeline.add(processorTypes.Set);
newPipeline.add(processorTypes.Set);
newPipeline.add(processorTypes.Set);
pipeline.load(newPipeline);
expect(pipeline.addExisting.calledWith(newPipeline.processors[0])).to.be(true);
expect(pipeline.addExisting.calledWith(newPipeline.processors[1])).to.be(true);
expect(pipeline.addExisting.calledWith(newPipeline.processors[2])).to.be(true);
});
});
describe('remove', function () {
it('remove the specified processor from the processors collection', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
pipeline.remove(pipeline.processors[1]);
expect(pipeline.processors[0].processorId).to.be(processorIds[0]);
expect(pipeline.processors[1].processorId).to.be(processorIds[2]);
});
});
describe('add', function () {
it('should append new items to the processors collection', function () {
const pipeline = new Pipeline();
expect(pipeline.processors.length).to.be(0);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
expect(pipeline.processors.length).to.be(3);
});
it('should append assign each new processor a unique processorId', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const ids = pipeline.processors.map((p) => { return p.processorId; });
expect(_.uniq(ids).length).to.be(3);
});
it('added processors should be an instance of the type supplied', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
expect(pipeline.processors[0] instanceof processorTypes.Set).to.be(true);
expect(pipeline.processors[1] instanceof processorTypes.Set).to.be(true);
expect(pipeline.processors[2] instanceof processorTypes.Set).to.be(true);
});
});
describe('addExisting', function () {
it('should append new items to the processors collection', function () {
const pipeline = new Pipeline();
expect(pipeline.processors.length).to.be(0);
const testProcessor = new processorTypes.Set('foo');
pipeline.addExisting(testProcessor);
expect(pipeline.processors.length).to.be(1);
});
it('should instantiate an object of the same class as the object passed in', function () {
const pipeline = new Pipeline();
const testProcessor = new processorTypes.Set('foo');
pipeline.addExisting(testProcessor);
expect(pipeline.processors[0] instanceof processorTypes.Set).to.be(true);
});
it('the object added should be a different instance than the object passed in', function () {
const pipeline = new Pipeline();
const testProcessor = new processorTypes.Set('foo');
pipeline.addExisting(testProcessor);
expect(pipeline.processors[0]).to.not.be(testProcessor);
});
it('the object added should have the same property values as the object passed in (except id)', function () {
const pipeline = new Pipeline();
const testProcessor = new processorTypes.Set('foo');
testProcessor.foo = 'bar';
testProcessor.bar = 'baz';
pipeline.addExisting(testProcessor);
expect(pipeline.processors[0].foo).to.be('bar');
expect(pipeline.processors[0].bar).to.be('baz');
expect(pipeline.processors[0].processorId).to.not.be('foo');
});
});
describe('moveUp', function () {
it('should be able to move an item up in the array', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
const target = pipeline.processors[1];
pipeline.moveUp(target);
expect(pipeline.processors[0].processorId).to.be(processorIds[1]);
expect(pipeline.processors[1].processorId).to.be(processorIds[0]);
expect(pipeline.processors[2].processorId).to.be(processorIds[2]);
});
it('should be able to move the same item move than once', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
const target = pipeline.processors[2];
pipeline.moveUp(target);
pipeline.moveUp(target);
expect(pipeline.processors[0].processorId).to.be(processorIds[2]);
expect(pipeline.processors[1].processorId).to.be(processorIds[0]);
expect(pipeline.processors[2].processorId).to.be(processorIds[1]);
});
it('should not move the selected item past the top', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
const target = pipeline.processors[2];
pipeline.moveUp(target);
pipeline.moveUp(target);
pipeline.moveUp(target);
pipeline.moveUp(target);
pipeline.moveUp(target);
expect(pipeline.processors[0].processorId).to.be(processorIds[2]);
expect(pipeline.processors[1].processorId).to.be(processorIds[0]);
expect(pipeline.processors[2].processorId).to.be(processorIds[1]);
});
it('should not allow the top item to be moved up', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
const target = pipeline.processors[0];
pipeline.moveUp(target);
expect(pipeline.processors[0].processorId).to.be(processorIds[0]);
expect(pipeline.processors[1].processorId).to.be(processorIds[1]);
expect(pipeline.processors[2].processorId).to.be(processorIds[2]);
});
});
describe('moveDown', function () {
it('should be able to move an item down in the array', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
const target = pipeline.processors[1];
pipeline.moveDown(target);
expect(pipeline.processors[0].processorId).to.be(processorIds[0]);
expect(pipeline.processors[1].processorId).to.be(processorIds[2]);
expect(pipeline.processors[2].processorId).to.be(processorIds[1]);
});
it('should be able to move the same item move than once', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
const target = pipeline.processors[0];
pipeline.moveDown(target);
pipeline.moveDown(target);
expect(pipeline.processors[0].processorId).to.be(processorIds[1]);
expect(pipeline.processors[1].processorId).to.be(processorIds[2]);
expect(pipeline.processors[2].processorId).to.be(processorIds[0]);
});
it('should not move the selected item past the bottom', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
const target = pipeline.processors[0];
pipeline.moveDown(target);
pipeline.moveDown(target);
pipeline.moveDown(target);
pipeline.moveDown(target);
pipeline.moveDown(target);
expect(pipeline.processors[0].processorId).to.be(processorIds[1]);
expect(pipeline.processors[1].processorId).to.be(processorIds[2]);
expect(pipeline.processors[2].processorId).to.be(processorIds[0]);
});
it('should not allow the bottom item to be moved down', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
const target = pipeline.processors[2];
pipeline.moveDown(target);
expect(pipeline.processors[0].processorId).to.be(processorIds[0]);
expect(pipeline.processors[1].processorId).to.be(processorIds[1]);
expect(pipeline.processors[2].processorId).to.be(processorIds[2]);
});
});
describe('updateParents', function () {
it('should set the first processors parent to pipeline.input', function () {
const pipeline = new Pipeline();
pipeline.input = { foo: 'bar' };
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.processors.forEach(p => sinon.stub(p, 'setParent'));
pipeline.updateParents();
expect(pipeline.processors[0].setParent.calledWith(pipeline.input)).to.be(true);
});
it('should set non-first processors parent to previous processor', function () {
const pipeline = new Pipeline();
pipeline.input = { foo: 'bar' };
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.processors.forEach(p => sinon.stub(p, 'setParent'));
pipeline.updateParents();
expect(pipeline.processors[1].setParent.calledWith(pipeline.processors[0])).to.be(true);
expect(pipeline.processors[2].setParent.calledWith(pipeline.processors[1])).to.be(true);
expect(pipeline.processors[3].setParent.calledWith(pipeline.processors[2])).to.be(true);
});
it('should set pipeline.dirty', function () {
const pipeline = new Pipeline();
pipeline.updateParents();
expect(pipeline.dirty).to.be(true);
});
});
describe('getProcessorById', function () {
it('should return a processor when suppied its id', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
const processorIds = getProcessorIds(pipeline);
const actual = pipeline.getProcessorById(processorIds[2]);
const expected = pipeline.processors[2];
expect(actual).to.be(expected);
});
it('should throw an error if given an unknown id', function () {
const pipeline = new Pipeline();
expect(pipeline.getProcessorById).withArgs('foo').to.throwError();
});
});
describe('updateOutput', function () {
it('should set the output to input if first processor has error', function () {
const pipeline = new Pipeline();
pipeline.input = { bar: 'baz' };
pipeline.add(processorTypes.Set);
pipeline.processors[0].outputObject = { field1: 'value1' };
pipeline.processors[0].error = {}; //define an error
pipeline.updateOutput();
expect(pipeline.output).to.be(pipeline.input);
});
it('should set the output to the processor before the error on a compile error', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.processors[0].outputObject = { field1: 'value1' };
pipeline.processors[1].outputObject = { field1: 'value2' };
pipeline.processors[2].outputObject = { field1: 'value3' };
pipeline.updateOutput();
expect(pipeline.output).to.eql({ field1: 'value3' });
pipeline.processors[1].error = { compile: true }; //define a compile error
pipeline.processors[0].locked = true; //all other processors get locked.
pipeline.processors[2].locked = true; //all other processors get locked.
pipeline.updateOutput();
expect(pipeline.output).to.eql({ field1: 'value1' });
});
it('should set the output to the last processor with valid output if a processor has an error', function () {
const pipeline = new Pipeline();
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.add(processorTypes.Set);
pipeline.processors[0].outputObject = { field1: 'value1' };
pipeline.processors[1].outputObject = { field1: 'value2' };
pipeline.processors[2].outputObject = { field1: 'value3' };
pipeline.updateOutput();
expect(pipeline.output).to.eql({ field1: 'value3' });
pipeline.processors[2].error = {}; //define an error
pipeline.updateOutput();
expect(pipeline.output).to.eql({ field1: 'value2' });
pipeline.processors[1].error = {}; //define an error
pipeline.processors[2].error = undefined; //if processor[1] has an error,
pipeline.processors[2].locked = true; //subsequent processors will be locked.
pipeline.updateOutput();
expect(pipeline.output).to.eql({ field1: 'value1' });
});
it('should set output to be last processor output if processors exist', function () {
const pipeline = new Pipeline();
pipeline.input = { bar: 'baz' };
pipeline.add(processorTypes.Set);
const expected = { foo: 'bar' };
pipeline.processors[0].outputObject = expected;
pipeline.updateOutput();
expect(pipeline.output).to.be(expected);
});
it('should set output to be equal to input if no processors exist', function () {
const pipeline = new Pipeline();
pipeline.input = { bar: 'baz' };
pipeline.updateOutput();
expect(pipeline.output).to.be(pipeline.input);
});
it('should set pipeline.dirty', function () {
const pipeline = new Pipeline();
pipeline.updateParents();
expect(pipeline.dirty).to.be(true);
pipeline.updateOutput();
expect(pipeline.dirty).to.be(false);
});
});
// describe('applySimulateResults', function () { });
});

View file

@ -0,0 +1,21 @@
import _ from 'lodash';
export default function selectableArray(items, selectedItems) {
if (!_.isArray(items)) throw new Error('First argument must be an array');
if (!_.isArray(selectedItems)) throw new Error('Second argument must be an array');
return items.map((item) => {
const selected = _.find(selectedItems, (selectedItem) => {
return cleanItem(selectedItem) === cleanItem(item);
});
return {
title: item,
selected: !_.isUndefined(selected)
};
});
};
function cleanItem(item) {
return _.trim(item).toUpperCase();
}

View file

@ -0,0 +1,21 @@
import _ from 'lodash';
export default function keysDeep(object, base) {
let result = [];
let delimitedBase = base ? base + '.' : '';
_.forEach(object, (value, key) => {
var fullKey = delimitedBase + key;
if (_.isPlainObject(value)) {
result = result.concat(keysDeep(value, fullKey));
} else {
result.push(fullKey);
}
});
if (base) {
result.push(base);
}
return result;
};

View file

@ -0,0 +1,176 @@
import _ from 'lodash';
function updateProcessorOutputs(pipeline, simulateResults) {
simulateResults.forEach((result) => {
const processor = pipeline.getProcessorById(result.processorId);
processor.outputObject = _.get(result, 'output');
processor.error = _.get(result, 'error');
});
}
//Updates the error state of the pipeline and its processors
//If a pipeline compile error is returned, lock all processors but the error
//If a pipeline data error is returned, lock all processors after the error
function updateErrorState(pipeline) {
pipeline.hasCompileError = _.some(pipeline.processors, (processor) => {
return _.get(processor, 'error.compile');
});
_.forEach(pipeline.processors, processor => {
processor.locked = false;
});
const errorIndex = _.findIndex(pipeline.processors, 'error');
if (errorIndex === -1) return;
_.forEach(pipeline.processors, (processor, index) => {
if (pipeline.hasCompileError && index !== errorIndex) {
processor.locked = true;
}
if (!pipeline.hasCompileError && index > errorIndex) {
processor.locked = true;
}
});
}
function updateProcessorInputs(pipeline) {
pipeline.processors.forEach((processor) => {
//we don't want to change the inputObject if the parent processor
//is in error because that can cause us to lose state.
if (!_.get(processor, 'parent.error')) {
//the parent property of the first processor is set to the pipeline.input.
//In all other cases it is set to processor[index-1]
if (!processor.parent.processorId) {
processor.inputObject = _.cloneDeep(processor.parent);
} else {
processor.inputObject = _.cloneDeep(processor.parent.outputObject);
}
}
});
}
export default class Pipeline {
constructor() {
this.processors = [];
this.processorCounter = 0;
this.input = {};
this.output = undefined;
this.dirty = false;
this.hasCompileError = false;
}
get model() {
const pipeline = {
input: this.input,
processors: _.map(this.processors, processor => processor.model)
};
return pipeline;
}
setDirty() {
this.dirty = true;
}
load(pipeline) {
this.processors = [];
pipeline.processors.forEach((processor) => {
this.addExisting(processor);
});
}
remove(processor) {
const processors = this.processors;
const index = processors.indexOf(processor);
processors.splice(index, 1);
}
moveUp(processor) {
const processors = this.processors;
const index = processors.indexOf(processor);
if (index === 0) return;
const temp = processors[index - 1];
processors[index - 1] = processors[index];
processors[index] = temp;
}
moveDown(processor) {
const processors = this.processors;
const index = processors.indexOf(processor);
if (index === processors.length - 1) return;
const temp = processors[index + 1];
processors[index + 1] = processors[index];
processors[index] = temp;
}
addExisting(existingProcessor) {
const Type = existingProcessor.constructor;
const newProcessor = this.add(Type);
_.assign(newProcessor, _.omit(existingProcessor, 'processorId'));
return newProcessor;
}
add(ProcessorType) {
const processors = this.processors;
this.processorCounter += 1;
const processorId = `processor_${this.processorCounter}`;
const newProcessor = new ProcessorType(processorId);
processors.push(newProcessor);
return newProcessor;
}
updateParents() {
const processors = this.processors;
processors.forEach((processor, index) => {
let newParent;
if (index === 0) {
newParent = this.input;
} else {
newParent = processors[index - 1];
}
processor.setParent(newParent);
});
this.dirty = true;
}
getProcessorById(processorId) {
const result = _.find(this.processors, { processorId });
if (!result) {
throw new Error(`Could not find processor by id [${processorId}]`);
}
return result;
}
updateOutput() {
const processors = this.processors;
const errorIndex = _.findIndex(processors, 'error');
const goodProcessor = errorIndex === -1 ? _.last(processors) : processors[errorIndex - 1];
this.output = goodProcessor ? goodProcessor.outputObject : this.input;
this.dirty = false;
}
// Updates the state of the pipeline and processors with the results
// from an ingest simulate call.
applySimulateResults(simulateResults) {
updateProcessorOutputs(this, simulateResults);
updateErrorState(this);
updateProcessorInputs(this);
this.updateOutput();
}
}

View file

@ -0,0 +1,38 @@
import uiModules from 'ui/modules';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiAppend', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function processorUiChanged() {
pipeline.setDirty();
}
function splitValues(delimitedList) {
return delimitedList.split('\n');
}
function joinValues(valueArray) {
return valueArray.join('\n');
}
function updateValues() {
processor.values = splitValues($scope.values);
}
$scope.values = joinValues(processor.values);
$scope.$watch('values', updateValues);
$scope.$watch('processor.targetField', processorUiChanged);
$scope.$watchCollection('processor.values', processorUiChanged);
}
};
});

View file

@ -0,0 +1,8 @@
<div class="form-group">
<label>Target Field:</label>
<input type="text" class="form-control" ng-model="processor.targetField">
</div>
<div class="form-group">
<label>Values:</label><span> (line delimited)</span>
<textarea ng-model="values" class="form-control"></textarea>
</div>

View file

@ -0,0 +1,23 @@
import Processor from '../base/view_model';
export class Append extends Processor {
constructor(processorId) {
super(processorId, 'append', 'Append');
this.targetField = '';
this.values = [];
}
get description() {
const target = this.targetField || '?';
return `[${target}]`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
targetField: this.targetField || '',
values: this.values || []
};
}
};

View file

@ -0,0 +1,23 @@
export default class Processor {
constructor(processorId, typeId, title) {
if (!typeId || !title) {
throw new Error('Cannot instantiate the base Processor class.');
}
this.processorId = processorId;
this.title = title;
this.typeId = typeId;
this.collapsed = false;
this.parent = undefined;
this.inputObject = undefined;
this.outputObject = undefined;
this.error = undefined;
}
setParent(newParent) {
const oldParent = this.parent;
this.parent = newParent;
return (oldParent !== this.parent);
}
}

View file

@ -0,0 +1,43 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiConvert', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
$scope.fields = keysDeep(processor.inputObject);
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.types = ['auto', 'number', 'string', 'boolean'];
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
$scope.$watch('processor.type', processorUiChanged);
$scope.$watch('processor.targetField', processorUiChanged);
}
};
});

View file

@ -0,0 +1,24 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>
<div class="form-group">
<label>Type:</label>
<select
class="form-control"
ng-options="type as type for type in types"
ng-model="processor.type">
</select>
</div>
<div class="form-group">
<label>Target Field:</label>
<input type="text" class="form-control" ng-model="processor.targetField">
</div>

View file

@ -0,0 +1,28 @@
import _ from 'lodash';
import Processor from '../base/view_model';
export class Convert extends Processor {
constructor(processorId) {
super(processorId, 'convert', 'Convert');
this.sourceField = '';
this.targetField = '';
this.type = 'auto';
}
get description() {
const source = this.sourceField || '?';
const type = this.type || '?';
const target = this.targetField ? ` -> [${this.targetField}]` : '';
return `[${source}] to ${type}${target}`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || '',
targetField: this.targetField || '',
type: this.type || 'auto'
};
}
};

View file

@ -0,0 +1,58 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import createMultiSelectModel from '../../lib/create_multi_select_model';
import template from './view.html';
import './styles.less';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiDate', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope, debounce) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
$scope.fields = keysDeep(processor.inputObject);
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
const updateFormats = debounce(() => {
processor.formats = _($scope.formats)
.filter('selected')
.map('title')
.value();
$scope.customFormatSelected = _.includes(processor.formats, 'Custom');
processorUiChanged();
}, 200);
$scope.updateFormats = updateFormats;
$scope.formats = createMultiSelectModel(['ISO8601', 'UNIX', 'UNIX_MS', 'TAI64N', 'Custom'], processor.formats);
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
$scope.$watch('processor.customFormat', updateFormats);
$scope.$watch('processor.targetField', processorUiChanged);
$scope.$watch('processor.timezone', processorUiChanged);
$scope.$watch('processor.locale', processorUiChanged);
}
};
});

View file

@ -0,0 +1,5 @@
processor-ui-date {
.custom-date-format {
display: flex;
}
}

View file

@ -0,0 +1,74 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>
<div class="form-group">
<label>Target Field:</label>
<input type="text" class="form-control" ng-model="processor.targetField">
</div>
<div class="form-group">
<label>Formats:</label>
<div ng-repeat="format in formats">
<input
type="checkbox"
id="format_{{processor.processorId}}_{{$index}}"
ng-model="format.selected"
ng-click="updateFormats()" />
<label for="format_{{processor.processorId}}_{{$index}}">
{{format.title}}
<a
aria-label="Custom Date Format Help"
tooltip="Custom Date Format Help"
tooltip-append-to-body="true"
href="http://www.joda.org/joda-time/key_format.html"
target="_blank"
ng-show="format.title === 'Custom'">
<i aria-hidden="true" class="fa fa-question-circle"></i>
</a>
</label>
</div>
<div
class="custom-date-format"
ng-show="customFormatSelected">
<input
type="text"
class="form-control"
ng-model="processor.customFormat">
</div>
</div>
<div class="form-group">
<label>
Timezone:
<a
aria-label="Timezone Help"
tooltip="Timezone Help"
tooltip-append-to-body="true"
href="http://joda-time.sourceforge.net/timezones.html"
target="_blank">
<i aria-hidden="true" class="fa fa-question-circle"></i>
</a>
</label>
<input type="text" class="form-control" ng-model="processor.timezone"></div>
</div>
<div class="form-group">
<label>
Locale:
<a
aria-label="Locale Help"
tooltip="Locale Help"
tooltip-append-to-body="true"
href="https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html"
target="_blank">
<i aria-hidden="true" class="fa fa-question-circle"></i>
</a>
</label>
<input type="text" class="form-control" ng-model="processor.locale"></div>
</div>

View file

@ -0,0 +1,32 @@
import Processor from '../base/view_model';
export class Date extends Processor {
constructor(processorId) {
super(processorId, 'date', 'Date');
this.sourceField = '';
this.targetField = '@timestamp';
this.formats = [];
this.timezone = 'Etc/UTC';
this.locale = 'ENGLISH';
this.customFormat = '';
}
get description() {
const source = this.sourceField || '?';
const target = this.targetField || '?';
return `[${source}] -> [${target}]`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || '',
targetField: this.targetField || '',
formats: this.formats || [],
timezone: this.timezone || '',
locale: this.locale || '',
customFormat: this.customFormat || ''
};
}
};

View file

@ -0,0 +1,61 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
import './styles.less';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiGeoip', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
$scope.fields = keysDeep(processor.inputObject);
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
function splitValues(delimitedList) {
return delimitedList.split('\n');
}
function joinValues(valueArray) {
return valueArray.join('\n');
}
function updateDatabaseFields() {
const fieldsString = $scope.databaseFields.replace(/,/g, '\n');
processor.databaseFields = _(splitValues(fieldsString)).map(_.trim).compact().value();
$scope.databaseFields = joinValues(processor.databaseFields);
}
$scope.databaseFields = joinValues(processor.databaseFields);
$scope.$watch('databaseFields', updateDatabaseFields);
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
$scope.$watch('processor.targetField', processorUiChanged);
$scope.$watch('processor.databaseFile', processorUiChanged);
$scope.$watchCollection('processor.databaseFields', processorUiChanged);
}
};
});

View file

@ -0,0 +1,13 @@
processor-ui-geoip {
.advanced-section {
margin-top: 15px;
&-heading{
.btn {
background-color: transparent;
color: black;
border: transparent;
}
}
}
}

View file

@ -0,0 +1,42 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>
<div class="form-group">
<label>Target Field:</label>
<input type="text" class="form-control" ng-model="processor.targetField">
</div>
<div class="advanced-section">
<div class="form-group advanced-section-heading">
<button
ng-click="processor.advancedExpanded = !processor.advancedExpanded"
type="button"
class="btn btn-default btn-xs processor-ui-container-header-toggle">
<i
aria-hidden="true"
ng-class="{ 'fa-caret-down': processor.advancedExpanded, 'fa-caret-right': !processor.advancedExpanded }"
class="fa">
</i>
</button>
<label ng-click="processor.advancedExpanded = !processor.advancedExpanded">Advanced Settings</label>
</div>
<div ng-show="processor.advancedExpanded">
<div class="form-group">
<label>Database File:</label>
<input type="text" class="form-control" ng-model="processor.databaseFile">
</div>
<div class="form-group">
<label>Data Fields:</label><span> (line delimited)</span>
<textarea ng-model="databaseFields" class="form-control"></textarea>
</div>
</div>
</div>

View file

@ -0,0 +1,28 @@
import Processor from '../base/view_model';
export class GeoIp extends Processor {
constructor(processorId) {
super(processorId, 'geoip', 'Geo IP');
this.sourceField = '';
this.targetField = '';
this.databaseFile = '';
this.databaseFields = [];
}
get description() {
const source = this.sourceField || '?';
const target = this.targetField || '?';
return `[${source}] -> [${target}]`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || '',
targetField: this.targetField || '',
databaseFile: this.databaseFile || '',
databaseFields: this.databaseFields || []
};
}
};

View file

@ -0,0 +1,40 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiGrok', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
$scope.fields = keysDeep(processor.inputObject);
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
$scope.$watch('processor.pattern', processorUiChanged);
}
};
});

View file

@ -0,0 +1,16 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>
<div class="form-group">
<label>Pattern:</label>
<input type="text" class="form-control" ng-model="processor.pattern">
</div>

View file

@ -0,0 +1,30 @@
import _ from 'lodash';
import keysDeep from '../../lib/keys_deep';
import Processor from '../base/view_model';
export class Grok extends Processor {
constructor(processorId) {
super(processorId, 'grok', 'Grok');
this.sourceField = '';
this.pattern = '';
}
get description() {
const inputKeys = keysDeep(this.inputObject);
const outputKeys = keysDeep(this.outputObject);
const addedKeys = _.difference(outputKeys, inputKeys);
const added = addedKeys.sort().map(field => `[${field}]`).join(', ');
const source = this.sourceField || '?';
return `[${source}] -> ${added}`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || '',
pattern: this.pattern || ''
};
}
};

View file

@ -0,0 +1,41 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiGsub', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
$scope.fields = keysDeep(processor.inputObject);
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
$scope.$watch('processor.pattern', processorUiChanged);
$scope.$watch('processor.replacement', processorUiChanged);
}
};
});

View file

@ -0,0 +1,20 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>
<div class="form-group">
<label>Pattern:</label>
<input type="text" class="form-control" ng-model="processor.pattern">
</div>
<div class="form-group">
<label>Replacement:</label>
<input type="text" class="form-control" ng-trim="false" ng-model="processor.replacement">
</div>

View file

@ -0,0 +1,25 @@
import Processor from '../base/view_model';
export class Gsub extends Processor {
constructor(processorId) {
super(processorId, 'gsub', 'Gsub');
this.sourceField = '';
this.pattern = '';
this.replacement = '';
}
get description() {
const source = this.sourceField || '?';
return `[${source}] - /${this.pattern}/ -> '${this.replacement}'`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || '',
pattern: this.pattern || '',
replacement: this.replacement || ''
};
}
};

View file

@ -0,0 +1,14 @@
import './append/directive';
import './convert/directive';
import './date/directive';
import './geoip/directive';
import './grok/directive';
import './gsub/directive';
import './join/directive';
import './lowercase/directive';
import './remove/directive';
import './rename/directive';
import './set/directive';
import './split/directive';
import './trim/directive';
import './uppercase/directive';

View file

@ -0,0 +1,41 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiJoin', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
const allKeys = keysDeep(processor.inputObject);
$scope.fields = _.filter(allKeys, (key) => { return _.isArray(_.get(processor.inputObject, key)); });
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
$scope.$watch('processor.separator', processorUiChanged);
}
};
});

View file

@ -0,0 +1,16 @@
<div class="form-group">
<label>Array Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>
<div class="form-group">
<label>Separator:</label>
<input type="text" class="form-control" ng-trim="false" ng-model="processor.separator">
</div>

View file

@ -0,0 +1,24 @@
import Processor from '../base/view_model';
export class Join extends Processor {
constructor(processorId) {
super(processorId, 'join', 'Join');
this.sourceField = '';
this.separator = '';
}
get description() {
const source = this.sourceField || '?';
const separator = this.separator ? ` on '${this.separator}'` : '';
return `[${source}]${separator}`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || '',
separator: this.separator || ''
};
}
};

View file

@ -0,0 +1,39 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiLowercase', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
const allKeys = keysDeep(processor.inputObject);
$scope.fields = _.filter(allKeys, (key) => { return _.isString(_.get(processor.inputObject, key)); });
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
}
};
});

View file

@ -0,0 +1,12 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>

View file

@ -0,0 +1,21 @@
import Processor from '../base/view_model';
export class Lowercase extends Processor {
constructor(processorId) {
super(processorId, 'lowercase', 'Lowercase');
this.sourceField = '';
}
get description() {
const source = this.sourceField || '?';
return `[${source}]`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || ''
};
}
};

View file

@ -0,0 +1,38 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiRemove', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
$scope.fields = keysDeep(processor.inputObject);
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
}
};
});

View file

@ -0,0 +1,12 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>

View file

@ -0,0 +1,21 @@
import Processor from '../base/view_model';
export class Remove extends Processor {
constructor(processorId) {
super(processorId, 'remove', 'Remove');
this.sourceField = '';
}
get description() {
const source = this.sourceField || '?';
return `[${source}]`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || ''
};
}
};

View file

@ -0,0 +1,40 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiRename', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
$scope.fields = keysDeep(processor.inputObject);
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
$scope.$watch('processor.targetField', processorUiChanged);
}
};
});

View file

@ -0,0 +1,16 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>
<div class="form-group">
<label>Target Field:</label>
<input type="text" class="form-control" ng-model="processor.targetField">
</div>

View file

@ -0,0 +1,24 @@
import Processor from '../base/view_model';
export class Rename extends Processor {
constructor(processorId) {
super(processorId, 'rename', 'Rename');
this.sourceField = '';
this.targetField = '';
}
get description() {
const source = this.sourceField || '?';
const target = this.targetField || '?';
return `[${source}] -> [${target}]`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || '',
targetField: this.targetField || ''
};
}
};

View file

@ -0,0 +1,23 @@
import uiModules from 'ui/modules';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiSet', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.targetField', processorUiChanged);
$scope.$watch('processor.value', processorUiChanged);
}
};
});

View file

@ -0,0 +1,8 @@
<div class="form-group">
<label>Target Field:</label>
<input type="text" class="form-control" ng-model="processor.targetField">
</div>
<div class="form-group">
<label>Value:</label>
<input type="text" class="form-control" ng-trim="false" ng-model="processor.value">
</div>

View file

@ -0,0 +1,23 @@
import Processor from '../base/view_model';
export class Set extends Processor {
constructor(processorId) {
super(processorId, 'set', 'Set');
this.targetField = '';
this.value = '';
}
get description() {
const target = this.targetField || '?';
return `[${target}]`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
targetField: this.targetField || '',
value: this.value || ''
};
}
};

View file

@ -0,0 +1,41 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiSplit', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
const allKeys = keysDeep(processor.inputObject);
$scope.fields = _.filter(allKeys, (key) => { return _.isString(_.get(processor.inputObject, key)); });
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
$scope.$watch('processor.separator', processorUiChanged);
}
};
});

View file

@ -0,0 +1,16 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>
<div class="form-group">
<label>Separator:</label>
<input type="text" class="form-control" ng-trim="false" ng-model="processor.separator">
</div>

View file

@ -0,0 +1,24 @@
import Processor from '../base/view_model';
export class Split extends Processor {
constructor(processorId) {
super(processorId, 'split', 'Split');
this.sourceField = '';
this.separator = '';
}
get description() {
const source = this.sourceField || '?';
const separator = this.separator || '?';
return `[${source}] on '${separator}'`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || '',
separator: this.separator || ''
};
}
};

View file

@ -0,0 +1,39 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiTrim', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
const allKeys = keysDeep(processor.inputObject);
$scope.fields = _.filter(allKeys, (key) => { return _.isString(_.get(processor.inputObject, key)); });
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
}
};
});

View file

@ -0,0 +1,12 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>

View file

@ -0,0 +1,21 @@
import Processor from '../base/view_model';
export class Trim extends Processor {
constructor(processorId) {
super(processorId, 'trim', 'Trim');
this.sourceField = '';
}
get description() {
const source = this.sourceField || '?';
return `[${source}]`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || ''
};
}
};

View file

@ -0,0 +1,39 @@
import _ from 'lodash';
import uiModules from 'ui/modules';
import keysDeep from '../../lib/keys_deep';
import template from './view.html';
const app = uiModules.get('kibana');
//scope.processor, scope.pipeline are attached by the process_container.
app.directive('processorUiUppercase', function () {
return {
restrict: 'E',
template: template,
controller : function ($scope) {
const processor = $scope.processor;
const pipeline = $scope.pipeline;
function consumeNewInputObject() {
const allKeys = keysDeep(processor.inputObject);
$scope.fields = _.filter(allKeys, (key) => { return _.isString(_.get(processor.inputObject, key)); });
refreshFieldData();
}
function refreshFieldData() {
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
}
function processorUiChanged() {
pipeline.setDirty();
}
$scope.$watch('processor.inputObject', consumeNewInputObject);
$scope.$watch('processor.sourceField', () => {
refreshFieldData();
processorUiChanged();
});
}
};
});

View file

@ -0,0 +1,12 @@
<div class="form-group">
<label>Field:</label>
<select
class="form-control"
ng-options="field as field for field in fields"
ng-model="processor.sourceField">
</select>
</div>
<div class="form-group">
<label>Field Data:</label>
<pre>{{ fieldData }}</pre>
</div>

View file

@ -0,0 +1,21 @@
import Processor from '../base/view_model';
export class Uppercase extends Processor {
constructor(processorId) {
super(processorId, 'uppercase', 'Uppercase');
this.sourceField = '';
}
get description() {
const source = this.sourceField || '?';
return `[${source}]`;
}
get model() {
return {
processorId: this.processorId,
typeId: this.typeId,
sourceField: this.sourceField || ''
};
}
};

View file

@ -0,0 +1,14 @@
export { Append } from './append/view_model';
export { Convert } from './convert/view_model';
export { Date } from './date/view_model';
export { GeoIp } from './geoip/view_model';
export { Grok } from './grok/view_model';
export { Gsub } from './gsub/view_model';
export { Join } from './join/view_model';
export { Lowercase } from './lowercase/view_model';
export { Remove } from './remove/view_model';
export { Rename } from './rename/view_model';
export { Set } from './set/view_model';
export { Split } from './split/view_model';
export { Trim } from './trim/view_model';
export { Uppercase } from './uppercase/view_model';

View file

@ -0,0 +1,28 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import (reference) "~ui/styles/theme";
output-preview {
.visual {
border: none;
background-color: @settings-filebeat-wizard-panel-bg;
border-radius: 0;
overflow-x: auto;
}
.visual.collapsed {
max-height: 125px;
overflow-y: auto;
}
pre {
background-color: transparent;
border: none;
}
.hide-unchanged {
.jsondiffpatch-unchanged {
display: none;
}
}
}

View file

@ -0,0 +1,22 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import (reference) "~ui/styles/theme";
pipeline-output {
flex: 1 1 auto;
display: flex;
flex-direction: column;
.header-line {
display: flex;
label {
width: 100%;
}
}
pre {
min-height: 450px;
flex: 1 1 1px;
}
}

View file

@ -0,0 +1,74 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import (reference) "~ui/styles/theme";
pipeline-setup {
.main-panels {
display: flex;
margin-bottom: 10px;
.left-panel {
.flex-parent(1, 1, 1px);
width: 50%;
&>label {
margin-bottom: 2px;
}
}
.center-panel {
.flex-parent(0, 0, auto, column);
justify-content: center;
.buttons {
.flex-parent(0, 0, auto, column);
}
}
.right-panel {
.flex-parent(1, 1, 1px, column);
width: 50%;
}
.pipeline {
min-height: 450px;
background-color: @settings-filebeat-wizard-panel-bg;
}
}
label {
margin-bottom: 0px;
}
ul.pipeline-container {
list-style-type: none;
padding: 0px;
margin-bottom: 0px;
&>li {
padding: 1px;
}
}
.add-processor {
padding:10px;
margin-bottom: 10px;
&-dropdown {
display: flex;
justify-content: flex-start;
align-items: center;
select.form-control {
background-color: @settings-filebeat-wizard-processor-select-bg;
border: none;
width: auto;
margin-right: 5px;
}
}
}
textarea.form-control {
min-height: 150px;
}
}

View file

@ -0,0 +1,42 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import (reference) "~ui/styles/theme";
processor-ui-container {
display: block;
margin-bottom: 1px;
border-bottom: 2px solid;
border-color: white;
.processor-ui-container-body {
display: block;
overflow: hidden;
position: relative;
&-content {
padding: 10px;
background-color: white;
}
.overlay {
display: none;
position: absolute;
top: -5000px;
left: -5000px;
width: 10000px;
height: 10000px;
background-color: @settings-filebeat-wizard-processor-container-overlay-bg;
}
&.locked {
.overlay {
display: block;
}
}
}
label {
font-weight: normal;
}
}

View file

@ -0,0 +1,47 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import (reference) "~ui/styles/theme";
processor-ui-container-header {
.processor-ui-container-header {
display: flex;
align-items: center;
flex: 1 0 auto;
background-color: @settings-filebeat-wizard-panel-bg;
border: none;
padding: 10px;
button {
width: 22px;
border-radius: 4px;
}
&-toggle {
flex: 0 0 auto;
margin-right: 5px;
}
&-title {
flex: 1 1 auto;
.ellipsis();
font-weight: bold;
.processor-title {
width: 100%;
}
.processor-description {
font-weight: normal;
}
.processor-description.danger {
font-weight: bold;
color: @brand-danger;
}
}
&-controls {
flex: 0 0 auto;
}
}
}

View file

@ -0,0 +1,17 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import (reference) "~ui/styles/theme";
source-data {
flex: 1 0 auto;
display: flex;
height: 22px;
button {
flex: 0 0 auto;
width: 22px;
position: relative;
top: -5px;
margin-left: 5px;
}
}

View file

@ -0,0 +1,24 @@
<div class="form-group">
<label>Processor Changes:</label>
<a
style="float: right"
ng-click="collapsed = true"
ng-hide="collapsed">collapse</a>
<a
style="float: right"
ng-click="collapsed = false"
ng-show="collapsed">expand</a>
<span style="float: right">&nbsp;/&nbsp;</span>
<a
style="float: right"
ng-click="showAll = false"
ng-show="showAll">only show changes</a>
<a
style="float: right"
ng-click="showAll = true"
ng-hide="showAll">show all</a>
<div
class="visual"
ng-class="{'hide-unchanged': !showAll, collapsed: collapsed}"></div>
</div>

View file

@ -0,0 +1,18 @@
<div class="header-line">
<label>
Pipeline Output
<a
aria-label="The pipeline output shows the result of the defined pipeline using the sample records supplied in the previous step."
tooltip="The pipeline output shows the result of the defined pipeline using the sample records supplied in the previous step."
tooltip-append-to-body="true"
target="_blank">
<i aria-hidden="true" class="fa fa-question-circle"></i>
</a>
</label>
<source-data
sample="sample"
samples="samples"
disabled="pipeline.hasCompileError">
</source-data>
</div>
<pre class="output">{{ pipeline.output | json }}</pre>

View file

@ -0,0 +1,74 @@
<h2>
<em>Let's build a pipeline!</em> Ingest pipelines are an easy way to modify documents before they're indexed in Elasticsearch. They're composed of processors which can change your data in many ways. Create a pipeline below while cycling through your samples to see its effect on your data.
</h2>
<div class="main-panels">
<div
ng-hide="expandContext < 1"
class="left-panel">
<label>
Processor Pipeline
<a
aria-label="A pipeline is a definition of a series of processors that are to be executed in the same order as they are declared."
tooltip="A pipeline is a definition of a series of processors that are to be executed in the same order as they are declared."
tooltip-append-to-body="true"
target="_blank">
<i aria-hidden="true" class="fa fa-question-circle"></i>
</a>
</label>
<div class="pipeline">
<ul
class="pipeline-container"
ng-show="pipeline.processors.length > 0">
<li ng-repeat="processor in pipeline.processors track by processor.processorId">
<processor-ui-container pipeline="pipeline" processor="processor"></processor-ui-container>
</li>
</ul>
<div class="add-processor">
<div
class="form-group"
ng-hide="pipeline.processors.length > 0">
<label>
Your pipeline is currently empty. Add a processor to get started!
</label>
</div>
<div class="add-processor-dropdown">
<select
class="form-control"
ng-options="processorType.title for processorType in processorTypes"
ng-model="processorType"
ng-disabled="pipeline.hasCompileError">
<option value="">Select a Processor...</option>
</select>
</div>
</div>
</div>
</div>
<div class="center-panel">
<div class="buttons">
<button
aria-label="{{expandContext > 1 ? 'Expand Right Panel' : 'Collapse Left Panel'}}"
tooltip="{{expandContext > 1 ? 'Expand Right Panel' : 'Collapse Left Panel'}}"
ng-click="expandContext = expandContext - 1"
ng-disabled="expandContext < 1"
type="button"
class="btn btn-primary btn-xs collapser">
<i aria-hidden="true" class="fa fa-chevron-circle-left"></i>
</button>
<button
aria-label="{{expandContext < 1 ? 'Expand Left Panel' : 'Collapse Right Panel'}}"
tooltip="{{expandContext < 1 ? 'Expand Left Panel' : 'Collapse Right Panel'}}"
ng-click="expandContext = expandContext + 1"
ng-disabled="expandContext > 1"
type="button"
class="btn btn-primary btn-xs collapser">
<i aria-hidden="true" class="fa fa-chevron-circle-right"></i>
</button>
</div>
</div>
<div
ng-hide="expandContext > 1"
class="right-panel">
<pipeline-output pipeline="pipeline" samples="samples" sample="sample"></pipeline-output>
</div>
</div>

View file

@ -0,0 +1,25 @@
<processor-ui-container-header
processor="processor"
field="sourceField"
pipeline="pipeline">
</processor-ui-container-header>
<div
class="processor-ui-container-body"
ng-class="{locked: processor.locked}">
<div
class="processor-ui-container-body-content"
ng-hide="processor.collapsed">
<div
ng-show="processor.error"
class="alert alert-danger">
{{processor.error.message}}
</div>
<div class="processor-ui-content"></div>
<output-preview
new-object="processor.outputObject"
old-object="processor.inputObject"
error="processor.error">
</output-preview>
</div>
<div class="overlay"></div>
</div>

View file

@ -0,0 +1,61 @@
<div class="processor-ui-container-header">
<button
aria-label="{{ processor.collapsed ? 'Expand Processor' : 'Collapse Processor' }}"
tooltip="{{ processor.collapsed ? 'Expand Processor' : 'Collapse Processor' }}"
tooltip-append-to-body="true"
ng-click="processor.collapsed = !processor.collapsed"
type="button"
class="btn btn-primary btn-xs processor-ui-container-header-toggle">
<i aria-hidden="true" ng-class="{ 'fa-caret-down': !processor.collapsed, 'fa-caret-right': processor.collapsed }" class="fa"></i>
</button>
<div class="processor-ui-container-header-title">
<span class="processor-title">
{{processor.title}}
</span>
<span class="processor-description">
- {{ processor.description }}
</span>
<!-- error -->
<span ng-if="processor.error" class="processor-description danger">
- Error
</span>
</div>
<div class="processor-ui-container-header-controls btn-group">
<button
aria-label="Increase Priority"
tooltip="Increase Priority"
tooltip-append-to-body="true"
ng-click="pipeline.moveUp(processor)"
type="button"
class="btn btn-xs btn-primary"
ng-disabled="pipeline.hasCompileError">
<i aria-hidden="true" class="fa fa-caret-up"></i>
</button>
<button
aria-label="Decrease Priority"
tooltip="Decrease Priority"
tooltip-append-to-body="true"
ng-click="pipeline.moveDown(processor)"
type="button"
class="btn btn-xs btn-primary"
ng-disabled="pipeline.hasCompileError">
<i aria-hidden="true" class="fa fa-caret-down"></i>
</button>
<button
aria-label="Remove Processor"
tooltip="Remove Processor"
tooltip-append-to-body="true"
ng-click="pipeline.remove(processor)"
type="button"
class="btn btn-xs btn-danger"
ng-disabled="pipeline.hasCompileError && !processor.error">
<i aria-hidden="true" class="fa fa-times"></i>
</button>
</div>
</div>

View file

@ -0,0 +1,20 @@
<button
aria-label="Previous Sample"
tooltip="Previous Sample"
tooltip-append-to-body="true"
ng-click="previousLine()"
type="button"
class="btn btn-xs btn-primary"
ng-disabled="disabled">
<i aria-hidden="true" class="fa fa-caret-left"></i>
</button>
<button
aria-label="Next Sample"
tooltip="Next Sample"
tooltip-append-to-body="true"
ng-click="nextLine()"
type="button"
class="btn btn-xs btn-primary"
ng-disabled="disabled">
<i aria-hidden="true" class="fa fa-caret-right"></i>
</button>

View file

@ -0,0 +1,22 @@
@import (reference) "../../../styles/_add_data_wizard";
@add-data-upload-step-multi-alert-padding: 2px;
.bulk-results {
.alert-warning {
padding: @add-data-upload-step-multi-alert-padding;
}
ul.errors {
background-color: white;
color: @text-color;
padding: @alert-padding - @add-data-upload-step-multi-alert-padding;
list-style-position: inside;
}
.alert-title {
display: flex;
padding: @alert-padding - @add-data-upload-step-multi-alert-padding;
justify-content: space-between;
}
}

View file

@ -0,0 +1,29 @@
<div ng-if="!uploadStep.created && !uploadStep.displayErrors.length">
<h2><em>Sit back, relax, we'll take it from here.</em></h2>
<div class="loading-message well">
We're loading your data now. This may take some time if you selected a large file.
</div>
</div>
<div ng-if="uploadStep.created || !!uploadStep.displayErrors.length" class="bulk-results">
<h2><em>Upload complete.</em> Let's take a look:</h2>
<div ng-if="uploadStep.created" class="alert alert-success">
Created <strong>{{ uploadStep.created }}</strong> documents!<br/>
</div>
<div class="alert alert-warning" ng-if="!!uploadStep.displayErrors.length">
<div class="alert-title">
We encountered errors while indexing your data
<a
ng-if="uploadStep.displayErrors.length > uploadStep.defaultErrorLimit"
ng-click="uploadStep.showAllErrors = !uploadStep.showAllErrors">
{{uploadStep.showAllErrors ? "Show Less" : "Show More"}}
</a>
</div>
<ul class="errors">
<li ng-repeat="error in uploadStep.displayErrors">{{ error }}</li>
</ul>
</div>
</div>

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