Merge pull request #6497 from BigFunger/ingest-pipeline-setup-client

Ingest pipeline setup client
This commit is contained in:
Jim Unger 2016-03-18 08:56:49 -05:00
commit ad349b58ae
40 changed files with 1301 additions and 105 deletions

View file

@ -67,6 +67,7 @@
},
"dependencies": {
"@bigfunger/decompress-zip": "0.2.0-stripfix2",
"@bigfunger/jsondiffpatch": "0.1.38-webpack",
"@spalger/angular-bootstrap": "0.12.1",
"@spalger/filesaver": "1.1.2",
"@spalger/leaflet-draw": "0.2.3",

View file

@ -10,4 +10,3 @@
<div class="paste-samples">
<textarea ng-model="pasteStep.rawSamples" placeholder="Paste your sample log lines here, separated by a newline"></textarea>
</div>

View file

@ -3,56 +3,6 @@ const template = require('plugins/kibana/settings/sections/indices/add_data_step
const _ = require('lodash');
const editFieldTypeHTML = require('plugins/kibana/settings/sections/indices/partials/_edit_field_type.html');
const testData = {
message: '11/24/2015 ip=1.1.1.1 bytes=1234',
clientip: '1.1.1.1',
bytes: 1234,
geoip: {
lat: 37.3894,
lon: 122.0819
},
location: {
lat: 37.3894,
lon: 122.0819
},
'@timestamp': '2015-11-24T00:00:00.000Z',
otherdate: '2015-11-24T00:00:00.000Z',
codes: [1, 2, 3, 4]
};
const testPipeline = [
{
grok: {
field: 'message',
pattern: 'foo'
}
},
{
geoip: {
source_field: 'ip'
}
},
{
geoip: {
source_field: 'ip',
target_field: 'location'
}
},
{
date: {
match_field: 'initialDate',
match_formats: ['dd/MM/yyyy hh:mm:ss']
}
},
{
date: {
match_field: 'initialDate',
match_formats: ['dd/MM/yyyy hh:mm:ss'],
target_field: 'otherdate'
}
}
];
function pickDefaultTimeFieldName(dateFields) {
if (_.isEmpty(dateFields)) {
return undefined;
@ -66,29 +16,26 @@ modules.get('apps/settings')
return {
template: template,
scope: {
sampleDocs: '=',
indexPattern: '=',
pipeline: '='
pipeline: '=',
sampleDoc: '='
},
controllerAs: 'reviewStep',
bindToController: true,
controller: function ($scope, Private) {
this.sampleDocs = testData;
this.pipeline = testPipeline;
if (_.isUndefined(this.indexPattern)) {
this.indexPattern = {};
}
const knownFieldTypes = {};
this.dateFields = [];
this.pipeline.forEach((processor) => {
if (processor.geoip) {
const field = processor.geoip.target_field || 'geoip';
this.pipeline.model.processors.forEach((processor) => {
if (processor.typeId === 'geoip') {
const field = processor.targetField || 'geoip';
knownFieldTypes[field] = 'geo_point';
}
else if (processor.date) {
const field = processor.date.target_field || '@timestamp';
else if (processor.typeId === 'date') {
const field = processor.targetField || '@timestamp';
knownFieldTypes[field] = 'date';
this.dateFields.push(field);
}
@ -98,7 +45,7 @@ modules.get('apps/settings')
id: 'filebeat-*',
title: 'filebeat-*',
timeFieldName: pickDefaultTimeFieldName(this.dateFields),
fields: _.map(this.sampleDocs, (value, key) => {
fields: _.map(this.sampleDoc, (value, key) => {
let type = knownFieldTypes[key] || typeof value;
if (type === 'object' && _.isArray(value) && !_.isEmpty(value)) {
type = typeof value[0];
@ -126,7 +73,7 @@ modules.get('apps/settings')
const buildRows = () => {
this.rows = _.map(this.indexPattern.fields, (field) => {
const sampleValue = this.sampleDocs[field.name];
const sampleValue = this.sampleDoc[field.name];
return [
_.escape(field.name),
{

View file

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

View file

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

View file

@ -0,0 +1,76 @@
import uiModules from 'ui/modules';
import _ from 'lodash';
import Pipeline from '../lib/pipeline';
import angular from 'angular';
import * as ProcessorTypes from '../lib/processor_types';
import IngestProvider from 'ui/ingest';
import '../styles/_pipeline_setup.less';
import './pipeline_output';
import './source_data';
import './processor_ui';
const app = uiModules.get('kibana');
function buildProcessorTypeList() {
return _(ProcessorTypes)
.map(Type => {
const instance = new Type();
return {
typeId: instance.typeId,
title: instance.title,
Type
};
})
.compact()
.value();
}
app.directive('pipelineSetup', function () {
return {
restrict: 'E',
template: require('../views/pipeline_setup.html'),
scope: {
samples: '=',
pipeline: '='
},
controller: function ($scope, debounce, Private, Notifier) {
const ingest = Private(IngestProvider);
const notify = new Notifier({ location: `Ingest Pipeline Setup` });
$scope.processorTypes = _.sortBy(buildProcessorTypeList(), 'title');
$scope.sample = {};
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.dirty) return;
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('pipeline.dirty', simulatePipeline);
}
};
});

View file

@ -0,0 +1,2 @@
import './processor_ui_container';
import './processor_ui_set';

View file

@ -0,0 +1,33 @@
import uiModules from 'ui/modules';
import _ from 'lodash';
import '../styles/_processor_ui_container.less';
import './output_preview';
import './processor_ui_container_header';
const app = uiModules.get('kibana');
app.directive('processorUiContainer', function ($compile) {
return {
restrict: 'E',
scope: {
pipeline: '=',
processor: '='
},
template: require('../views/processor_ui_container.html'),
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,16 @@
import uiModules from 'ui/modules';
import '../styles/_processor_ui_container_header.less';
const app = uiModules.get('kibana');
app.directive('processorUiContainerHeader', function () {
return {
restrict: 'E',
scope: {
processor: '=',
field: '=',
pipeline: '='
},
template: require('../views/processor_ui_container_header.html')
};
});

View file

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

View file

@ -0,0 +1,43 @@
import uiModules from 'ui/modules';
import angular from 'angular';
import '../styles/_source_data.less';
const app = uiModules.get('kibana');
app.directive('sourceData', function () {
return {
restrict: 'E',
scope: {
samples: '=',
sample: '='
},
template: require('../views/source_data.html'),
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) return;
$scope.selectedSample = samples[currentIndex - 1];
};
$scope.nextLine = function () {
let currentIndex = samples.indexOf($scope.selectedSample);
if (currentIndex >= samples.length - 1) return;
$scope.selectedSample = samples[currentIndex + 1];
};
}
};
});

View file

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

View file

@ -1,7 +1,6 @@
var expect = require('expect.js');
var sinon = require('sinon');
var keysDeep = require('../keys_deep');
import expect from 'expect.js';
import sinon from 'sinon';
import keysDeep from '../keys_deep';
describe('keys deep', function () {

View file

@ -0,0 +1,436 @@
import _ from 'lodash';
import expect from 'expect.js';
import sinon from 'sinon';
import Pipeline from '../../lib/pipeline';
describe('processor pipeline', function () {
class TestProcessor {
constructor(processorId) {
this.processorId = processorId;
}
setParent(newParent) { }
}
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(TestProcessor);
pipeline.processors[0].model = { bar: 'baz' };
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
const oldProcessors = [ pipeline.processors[0], pipeline.processors[1], pipeline.processors[2] ];
const newPipeline = new Pipeline();
newPipeline.add(TestProcessor);
newPipeline.add(TestProcessor);
newPipeline.add(TestProcessor);
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(TestProcessor);
newPipeline.add(TestProcessor);
newPipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
expect(pipeline.processors.length).to.be(3);
});
it('should append assign each new processor a unique processorId', function () {
const pipeline = new Pipeline();
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
expect(pipeline.processors[0] instanceof TestProcessor).to.be(true);
expect(pipeline.processors[1] instanceof TestProcessor).to.be(true);
expect(pipeline.processors[2] instanceof TestProcessor).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 TestProcessor();
testProcessor.processorId = 'foo';
testProcessor.foo = 'bar';
testProcessor.bar = 'baz';
pipeline.addExisting(testProcessor);
expect(pipeline.processors.length).to.be(1);
});
it('should instanciate an object of the same class as the object passed in', function () {
const pipeline = new Pipeline();
const testProcessor = new TestProcessor();
testProcessor.processorId = 'foo';
testProcessor.foo = 'bar';
testProcessor.bar = 'baz';
pipeline.addExisting(testProcessor);
expect(pipeline.processors[0] instanceof TestProcessor).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 TestProcessor();
testProcessor.processorId = 'foo';
testProcessor.foo = 'bar';
testProcessor.bar = 'baz';
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 TestProcessor();
testProcessor.processorId = '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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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(TestProcessor);
pipeline.add(TestProcessor);
pipeline.add(TestProcessor);
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 output to be last processors output if processors exist', function () {
const pipeline = new Pipeline();
pipeline.add(TestProcessor);
const expected = { foo: 'bar' };
pipeline.processors[0].outputObject = expected;
pipeline.updateOutput();
expect(pipeline.output).to.be(expected);
});
it('should set output to be undefined if no processors exist', function () {
const pipeline = new Pipeline();
pipeline.updateOutput();
expect(pipeline.output).to.be(undefined);
});
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

@ -1,4 +1,4 @@
const _ = require('lodash');
import _ from 'lodash';
export default function keysDeep(object, base) {
let result = [];

View file

@ -0,0 +1,142 @@
import _ from 'lodash';
export default class Pipeline {
constructor() {
this.processors = [];
this.counter = 0;
this.input = {};
this.output = undefined;
this.dirty = false;
}
get model() {
return {
input: this.input,
processors: _.map(this.processors, processor => processor.model)
};
}
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.counter += 1;
const processorId = `processor_${this.counter}`;
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;
}
updateOutput() {
const processors = this.processors;
this.output = undefined;
if (processors.length > 0) {
this.output = processors[processors.length - 1].outputObject;
}
this.dirty = false;
}
getProcessorById(processorId) {
const result = _.find(this.processors, { processorId });
if (!result) {
throw new Error(`Could not find processor by id [${processorId}]`);
}
return result;
}
// Updates the state of the pipeline and processors with the results
// from an ingest simulate call.
applySimulateResults(results) {
//update the outputObject of each processor
results.forEach((result) => {
const processor = this.getProcessorById(result.processorId);
processor.outputObject = _.get(result, 'output');
processor.error = _.get(result, 'error');
});
//update the inputObject of each processor
results.forEach((result) => {
const processor = this.getProcessorById(result.processorId);
//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, 'error.isNested')) {
//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);
}
}
});
this.updateOutput();
}
}

View file

@ -0,0 +1,45 @@
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);
}
}
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,28 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import (reference) "~ui/styles/theme";
output-preview {
.visual {
background-color: @settings-pipeline-setup-output-preview-bg;
border: 1px solid;
border-color: @settings-pipeline-setup-output-preview-border;
overflow-x: auto;
}
.visual.collapsed {
max-height: 125px;
overflow-y: scroll;
}
pre {
background-color: transparent;
border: none;
}
.hide-unchanged {
.jsondiffpatch-unchanged {
display: none;
}
}
}

View file

@ -0,0 +1,19 @@
pipeline-output {
display: block;
.header-line {
display: flex;
label {
width: 100%;
}
}
pre {
min-height: 100px;
}
pre.collapsed {
max-height: 100px;
}
}

View file

@ -0,0 +1,29 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import (reference) "~ui/styles/theme";
pipeline-setup {
label {
margin-bottom: 0px;
}
ul.pipeline-container {
list-style-type: none;
padding: 0px;
&>li {
border: 1px solid;
border-color: @settings-pipeline-setup-pipeline-border;
}
}
.empty-pipeline {
border: 1px solid;
border-color: @settings-pipeline-setup-pipeline-border;
padding: 5px;
p {
font-size: 15px;
}
}
}

View file

@ -0,0 +1,40 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import (reference) "~ui/styles/theme";
processor-ui-container {
display: block;
margin-bottom: 1px;
.processor-ui-container-body {
display: block;
overflow: hidden;
position: relative;
&-content {
padding: 5px;
}
.overlay {
display: none;
position: absolute;
top: -5000px;
left: -5000px;
width: 10000px;
height: 10000px;
background-color: @settings-pipeline-setup-processor-container-overlay-bg;
}
&.dirty {
.overlay {
display: block;
}
}
}
.form-group {
margin-bottom: 5px;
}
}

View file

@ -0,0 +1,42 @@
@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-pipeline-setup-processor-container-overlay-bg;
border-bottom: 1px solid;
border-bottom-color: @settings-pipeline-setup-processor-container-header-border;
&-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,5 @@
processor-ui-date {
.custom-date-format {
display: flex;
}
}

View file

@ -0,0 +1,10 @@
source-data {
button {
width: 40px;
margin-left: 5px;
}
div.controls {
display: flex;
}
}

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,12 @@
<div class="form-group">
<div class="header-line">
<label>Pipeline Output:</label>
<a
ng-click="collapsed = false"
ng-show="collapsed">expand</a>
<a
ng-click="collapsed = true"
ng-hide="collapsed">collapse</a>
</div>
<pre class="output" ng-class="{ collapsed: collapsed }">{{ pipeline.output | json }}</pre>
</div>

View file

@ -0,0 +1,33 @@
<source-data sample="sample" samples="samples"></source-data>
<pipeline-output pipeline="pipeline"></pipeline-output>
<div class="form-group">
<label>Processor Pipeline:</label>
<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="empty-pipeline"
ng-hide="pipeline.processors.length > 0">
<p>Your pipeline is currently empty. Add a processor to get started!</p>
</div>
</div>
<div class="form-group">
<label>Processor Type:</label>
<select
class="form-control"
ng-options="processorType.title for processorType in processorTypes"
ng-model="processorType">
</select>
</div>
<button
ng-click="pipeline.add(processorType.Type)"
ng-disabled="!processorType">
Add Processor
</button>

View file

@ -0,0 +1,21 @@
<processor-ui-container-header
processor="processor"
field="sourceField"
pipeline="pipeline">
</processor-ui-container-header>
<div
class="processor-ui-container-body"
ng-class="{dirty: processor.error.isNested}">
<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"></output-preview>
</div>
<div class="overlay"></div>
</div>

View file

@ -0,0 +1,58 @@
<div class="processor-ui-container-header">
<button
aria-label="{{ processor.collapsed ? 'Open Editor' : 'Close Editor' }}"
tooltip="{{ processor.collapsed ? 'Open Editor' : 'Close Editor' }}"
tooltip-append-to-body="true"
ng-click="processor.collapsed = !processor.collapsed"
type="button"
class="btn btn-default 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-default">
<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-default">
<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">
<i aria-hidden="true" class="fa fa-times"></i>
</button>
</div>
</div>

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,28 @@
<div class="form-group">
<label>Current Line:</label>
<div class="controls">
<select
class="form-control"
ng-options="sample.message for sample in samples"
ng-model="selectedSample">
</select>
<button
aria-label="Previous Line"
tooltip="Previous Line"
tooltip-append-to-body="true"
ng-click="previousLine()"
type="button"
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-chevron-left"></i>
</button>
<button
aria-label="Next Line"
tooltip="Next Line"
tooltip-append-to-body="true"
ng-click="nextLine()"
type="button"
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-chevron-right"></i>
</button>
</div>
</div>

View file

@ -1,7 +0,0 @@
<h2>Build pipeline step</h2>
<div>
Logs: {{samples}}
</div>
<button ng-click="sampleDocs = {results: {os: 'osx'}}; pipeline = {processor: 'I processor'};">Build a pipeline</button>

View file

@ -1,15 +0,0 @@
var modules = require('ui/modules');
var template = require('plugins/kibana/settings/sections/indices/add_data_steps/pipeline_step.html');
modules.get('apps/settings')
.directive('pipelineStep', function () {
return {
template: template,
scope: {
samples: '=',
sampleDocs: '=',
pipeline: '='
}
};
});

View file

@ -38,23 +38,22 @@
</div>
<div ng-switch-when="1">
<pipeline-step
samples="wizard.stepResults.samples"
<pipeline-setup
pipeline="wizard.stepResults.pipeline"
sample-docs="wizard.stepResults.sampleDocs">
</pipeline-step>
samples="wizard.stepResults.samples">
</pipeline-setup>
<div class="nav-buttons">
<button ng-click="wizard.prevStep()">Prev</button>
<button ng-disabled="!wizard.stepResults.pipeline || !wizard.stepResults.sampleDocs" ng-click="wizard.nextStep()">Next</button>
<button ng-disabled="!wizard.stepResults.pipeline" ng-click="wizard.nextStep()">Next</button>
</div>
</div>
<div ng-switch-when="2">
<pattern-review-step
index-pattern="wizard.stepResults.indexPattern"
sample-docs="wizard.stepResults.sampleDocs"
pipeline="wizard.stepResults.pipeline">
pipeline="wizard.stepResults.pipeline"
sample-doc="wizard.stepResults.pipeline.output">
</pattern-review-step>
<div class="nav-buttons">

View file

@ -4,7 +4,7 @@ import IngestProvider from 'ui/ingest';
require('plugins/kibana/settings/sections/indices/add_data_steps/pattern_review_step');
require('plugins/kibana/settings/sections/indices/add_data_steps/paste_samples_step');
require('plugins/kibana/settings/sections/indices/add_data_steps/pipeline_step');
require('plugins/kibana/settings/sections/indices/add_data_steps/pipeline_setup');
require('plugins/kibana/settings/sections/indices/add_data_steps/install_filebeat_step');
// wrapper directive, which sets up the breadcrumb for all filebeat steps
@ -51,7 +51,8 @@ modules.get('apps/settings')
};
this.save = () => {
return ingest.save(this.stepResults.indexPattern, this.stepResults.pipeline)
const processors = this.stepResults.pipeline.processors.map(processor => processor.model);
return ingest.save(this.stepResults.indexPattern, processors)
.then(
() => {
this.nextStep();

View file

@ -180,6 +180,10 @@ kbn-settings-indices {
p.text-center {
padding-top: 1em;
}
.nav-buttons {
float:right;
}
}

View file

@ -2,10 +2,10 @@ import Joi from 'joi';
const base = Joi.object({
processor_id: Joi.string().required()
}).unknown();
});
export const set = base.keys({
type_id: Joi.string().only('set').required(),
target_field: Joi.string().required(),
value: Joi.any().required()
target_field: Joi.string().allow(''),
value: Joi.string().allow('')
});

View file

@ -1,5 +1,6 @@
import { keysToSnakeCaseShallow } from '../../../plugins/kibana/common/lib/case_conversion';
import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../plugins/kibana/common/lib/case_conversion';
import _ from 'lodash';
import angular from 'angular';
export default function IngestProvider($rootScope, $http, config) {
@ -14,7 +15,7 @@ export default function IngestProvider($rootScope, $http, config) {
index_pattern: keysToSnakeCaseShallow(indexPattern)
};
if (!_.isEmpty(pipeline)) {
payload.pipeline = pipeline;
payload.pipeline = _.map(pipeline, processor => keysToSnakeCaseShallow(processor));
}
return $http.post(`${ingestAPIPrefix}`, payload)
@ -38,4 +39,24 @@ export default function IngestProvider($rootScope, $http, config) {
});
};
this.simulate = function (pipeline) {
function pack(pipeline) {
const result = keysToSnakeCaseShallow(pipeline);
result.processors = _.map(result.processors, processor => keysToSnakeCaseShallow(processor));
return result;
}
function unpack(response) {
const data = response.data.map(result => keysToCamelCaseShallow(result));
return data;
}
return $http.post(`${ingestAPIPrefix}/simulate`, pack(pipeline))
.then(unpack)
.catch(err => {
throw ('Error communicating with Kibana server');
});
};
}

View file

@ -121,6 +121,14 @@
@settings-indices-active-color: @btn-success-bg;
// Settings - Add Data Wizard - Pipeline Setup =================================
@settings-pipeline-setup-output-preview-border: @gray12;
@settings-pipeline-setup-output-preview-bg: @gray-lighter;
@settings-pipeline-setup-pipeline-border: @gray12;
@settings-pipeline-setup-processor-container-overlay-bg: fade(#000, 10%);
@settings-pipeline-setup-processor-container-header-border: @gray12;
// Visualize ===================================================================
@visualize-show-spy-border: @gray-lighter;

View file

@ -25,7 +25,8 @@ define(function (require) {
processors: [{
processor_id: 'processor1',
type_id: 'set',
value: 'bar'
value: 'bar',
target_field: 42
}]
})
.expect(400)