mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
commit
15a4fa1cdd
213 changed files with 9707 additions and 179 deletions
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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/);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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');
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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');
|
||||
})
|
||||
|
|
|
@ -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
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
<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"/>
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.paste-samples {
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -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, '');
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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
|
||||
};
|
||||
});
|
|
@ -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];
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
import './directives/pipeline_setup';
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -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 () { });
|
||||
|
||||
|
||||
});
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || []
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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'
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
processor-ui-date {
|
||||
.custom-date-format {
|
||||
display: flex;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
processor-ui-geoip {
|
||||
.advanced-section {
|
||||
margin-top: 15px;
|
||||
|
||||
&-heading{
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
border: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 || []
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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';
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
|
@ -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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"> / </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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue