diff --git a/package.json b/package.json index 4374c5056995..4fcc9db19f0b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/paste_samples_step.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/paste_samples_step.html index b24c6b74094f..382b31765d07 100644 --- a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/paste_samples_step.html +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/paste_samples_step.html @@ -10,4 +10,3 @@
- diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step.js index afc4161bcf85..d56833e44402 100644 --- a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step.js +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step.js @@ -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), { diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/output_preview.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/output_preview.js new file mode 100644 index 000000000000..9855414093de --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/output_preview.js @@ -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); + } + }; +}); diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/pipeline_output.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/pipeline_output.js new file mode 100644 index 000000000000..fb1e2e707a85 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/pipeline_output.js @@ -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; + } + }; +}); diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/pipeline_setup.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/pipeline_setup.js new file mode 100644 index 000000000000..d1ff7aa01a34 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/pipeline_setup.js @@ -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); + } + }; +}); diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui.js new file mode 100644 index 000000000000..ae2542846547 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui.js @@ -0,0 +1,2 @@ +import './processor_ui_container'; +import './processor_ui_set'; diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui_container.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui_container.js new file mode 100644 index 000000000000..7e118b6e6df0 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui_container.js @@ -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 = ``; + const $innerEl = $compile(template)(newScope); + + $innerEl.appendTo($container); + } + }; +}); diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui_container_header.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui_container_header.js new file mode 100644 index 000000000000..77cf33c0cf1f --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui_container_header.js @@ -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') + }; +}); diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui_set.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui_set.js new file mode 100644 index 000000000000..76b58a6a7d2d --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/processor_ui_set.js @@ -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); + } + }; +}); diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/source_data.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/source_data.js new file mode 100644 index 000000000000..b4335eaed742 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/directives/source_data.js @@ -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]; + }; + } + }; +}); diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/index.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/index.js new file mode 100644 index 000000000000..63e0035c1ca7 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/index.js @@ -0,0 +1 @@ +import './directives/pipeline_setup'; diff --git a/src/plugins/kibana/common/lib/_tests_/keys_deep.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/__tests__/keys_deep.js similarity index 94% rename from src/plugins/kibana/common/lib/_tests_/keys_deep.js rename to src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/__tests__/keys_deep.js index c28224058a47..05f06927a435 100644 --- a/src/plugins/kibana/common/lib/_tests_/keys_deep.js +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/__tests__/keys_deep.js @@ -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 () { diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/__tests__/pipeline.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/__tests__/pipeline.js new file mode 100644 index 000000000000..a276640ce42e --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/__tests__/pipeline.js @@ -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 () { }); + + +}); diff --git a/src/plugins/kibana/common/lib/keys_deep.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/keys_deep.js similarity index 93% rename from src/plugins/kibana/common/lib/keys_deep.js rename to src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/keys_deep.js index b0eefb3827be..6485f55344d8 100644 --- a/src/plugins/kibana/common/lib/keys_deep.js +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/keys_deep.js @@ -1,4 +1,4 @@ -const _ = require('lodash'); +import _ from 'lodash'; export default function keysDeep(object, base) { let result = []; diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/pipeline.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/pipeline.js new file mode 100644 index 000000000000..7f381b27ba51 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/pipeline.js @@ -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(); + } + +} diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/processor_types.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/processor_types.js new file mode 100644 index 000000000000..10e3f51f3a63 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/lib/processor_types.js @@ -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 + }; + } +}; diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_output_preview.less b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_output_preview.less new file mode 100644 index 000000000000..b1ef4b9e8c78 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_output_preview.less @@ -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; + } + } +} diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_pipeline_output.less b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_pipeline_output.less new file mode 100644 index 000000000000..3cb27352985a --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_pipeline_output.less @@ -0,0 +1,19 @@ +pipeline-output { + display: block; + + .header-line { + display: flex; + + label { + width: 100%; + } + } + + pre { + min-height: 100px; + } + + pre.collapsed { + max-height: 100px; + } +} diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_pipeline_setup.less b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_pipeline_setup.less new file mode 100644 index 000000000000..2f558fcb47a5 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_pipeline_setup.less @@ -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; + } + } +} diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_processor_ui_container.less b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_processor_ui_container.less new file mode 100644 index 000000000000..eeb5f6e655c3 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_processor_ui_container.less @@ -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; + } + +} diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_processor_ui_container_header.less b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_processor_ui_container_header.less new file mode 100644 index 000000000000..ce262d901f94 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_processor_ui_container_header.less @@ -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; + } + } +} diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_processor_ui_date.less b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_processor_ui_date.less new file mode 100644 index 000000000000..9b28eb3adc86 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_processor_ui_date.less @@ -0,0 +1,5 @@ +processor-ui-date { + .custom-date-format { + display: flex; + } +} diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_source_data.less b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_source_data.less new file mode 100644 index 000000000000..c250d9761f83 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/styles/_source_data.less @@ -0,0 +1,10 @@ +source-data { + button { + width: 40px; + margin-left: 5px; + } + + div.controls { + display: flex; + } +} diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/output_preview.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/output_preview.html new file mode 100644 index 000000000000..00622f43c4e7 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/output_preview.html @@ -0,0 +1,24 @@ +
+ + + collapse + expand +  /  + only show changes + show all +
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/pipeline_output.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/pipeline_output.html new file mode 100644 index 000000000000..e32b720dc770 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/pipeline_output.html @@ -0,0 +1,12 @@ +
+
+ + expand + collapse +
+
{{ pipeline.output | json }}
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/pipeline_setup.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/pipeline_setup.html new file mode 100644 index 000000000000..954cd3b6b09b --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/pipeline_setup.html @@ -0,0 +1,33 @@ + + + + +
+ + +
+

Your pipeline is currently empty. Add a processor to get started!

+
+
+ +
+ + +
+ diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/processor_ui_container.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/processor_ui_container.html new file mode 100644 index 000000000000..ef3878385198 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/processor_ui_container.html @@ -0,0 +1,21 @@ + + +
+
+
+ {{processor.error.message}} +
+
+ +
+
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/processor_ui_container_header.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/processor_ui_container_header.html new file mode 100644 index 000000000000..c5e7aa9e582d --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/processor_ui_container_header.html @@ -0,0 +1,58 @@ +
+ + +
+ + {{processor.title}} + + + + - {{ processor.description }} + + + + + - Error + +
+ +
+ + + + + +
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/processor_ui_set.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/processor_ui_set.html new file mode 100644 index 000000000000..c6459b62f384 --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/processor_ui_set.html @@ -0,0 +1,8 @@ +
+ + +
+
+ + +
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/source_data.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/source_data.html new file mode 100644 index 000000000000..f0309b639eba --- /dev/null +++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_setup/views/source_data.html @@ -0,0 +1,28 @@ +
+ +
+ + + +
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_step.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_step.html deleted file mode 100644 index 0c717c86256b..000000000000 --- a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_step.html +++ /dev/null @@ -1,7 +0,0 @@ -

Build pipeline step

- -
- Logs: {{samples}} -
- - diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_step.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_step.js deleted file mode 100644 index fc946f14f40a..000000000000 --- a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pipeline_step.js +++ /dev/null @@ -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: '=' - } - }; - }); - diff --git a/src/plugins/kibana/public/settings/sections/indices/filebeat/directives/filebeat_wizard.html b/src/plugins/kibana/public/settings/sections/indices/filebeat/directives/filebeat_wizard.html index e81e9e1bfc4c..ffb3556db4ef 100644 --- a/src/plugins/kibana/public/settings/sections/indices/filebeat/directives/filebeat_wizard.html +++ b/src/plugins/kibana/public/settings/sections/indices/filebeat/directives/filebeat_wizard.html @@ -38,23 +38,22 @@
- - + samples="wizard.stepResults.samples"> +
+ pipeline="wizard.stepResults.pipeline" + sample-doc="wizard.stepResults.pipeline.output">