added the management section refactoring from master

This commit is contained in:
Stéphane Campinas 2016-06-30 10:14:19 +01:00
commit 9e32bbc2cf
491 changed files with 13506 additions and 3103 deletions

1
.gitignore vendored
View file

@ -15,6 +15,7 @@ target
/test/screenshots/diff
/test/screenshots/failure
/test/screenshots/session
/test/screenshots/visual_regression_gallery.html
/esvm
.htpasswd
.eslintcache

View file

@ -113,7 +113,7 @@ Once that is complete just run:
```
sh
npm run test && npm run build
npm run test && npm run build -- --skip-os-packages
```
#### Debugging unit tests
@ -121,55 +121,41 @@ npm run test && npm run build
The standard `npm run test` task runs several sub tasks and can take several minutes to complete, making debugging failures pretty painful. In order to ease the pain specialized tasks provide alternate methods for running the tests.
`npm run test:quick`
`npm run test:quick`
Runs both server and browser tests, but skips linting
`npm run test:server`
`npm run test:server`
Run only the server tests
`npm run test:browser`
`npm run test:browser`
Run only the browser tests. Coverage reports are available for browser tests by running `npm run test:coverage`. You can find the results under the `coverage/` directory that will be created upon completion.
`npm run test:dev`
Initializes an environment for debugging the browser tests. Includes an dedicated instance of the kibana server for building the test bundle, and a karma server. When running this task the build is optimized for the first time and then a karma-owned instance of the browser is opened. Click the "debug" button to open a new tab that executes the unit tests.
`npm run test:dev`
Initializes an environment for debugging the browser tests. Includes an dedicated instance of the kibana server for building the test bundle, and a karma server. When running this task the build is optimized for the first time and then a karma-owned instance of the browser is opened. Click the "debug" button to open a new tab that executes the unit tests.
![Browser test debugging](http://i.imgur.com/DwHxgfq.png)
`npm run mocha [test file or dir]` or `npm run mocha:debug [test file or dir]`
`npm run mocha [test file or dir]` or `npm run mocha:debug [test file or dir]`
Run a one off test with the local project version of mocha, babel compilation, and optional debugging. Great
for development and fixing individual tests.
#### Unit testing plugins
This should work super if you're using the [Kibana plugin generator](https://github.com/elastic/generator-kibana-plugin). If you're not using the generator, well, you're on your own. We suggest you look at how the generator works.
`npm run test:dev -- --kbnServer.testsBundle.pluginId=some_special_plugin --kbnServer.plugin-path=../some_special_plugin`
`npm run test:dev -- --kbnServer.testsBundle.pluginId=some_special_plugin --kbnServer.plugin-path=../some_special_plugin`
Run the tests for just your particular plugin. Assuming you plugin lives outside of the `installedPlugins directory`, which it should.
#### Running browser automation tests:
*The Selenium server that is started currently only runs the tests in a recent version of Firefox.*
*You can use the `PATH` environment variable to specify which version of Firefox to use.*
The following will start Kibana, Elasticsearch and the chromedriver for you. To run the functional UI tests use the following commands
The following will start Kibana, Elasticsearch and Selenium for you. To run the functional UI tests use the following commands
`npm run test:ui`
`npm run test:ui`
Run the functional UI tests one time and exit. This is used by the CI systems and is great for quickly checking that things pass. It is essentially a combination of the next two tasks.
`npm run test:ui:server`
`npm run test:ui:server`
Start the server required for the `test:ui:runner` tasks. Once the server is started `test:ui:runner` can be run multiple times without waiting for the server to start.
`npm run test:ui:runner`
Execute the front-end selenium tests. This requires the server started by the `test:ui:server` task.
##### If you already have ElasticSearch, Kibana, and Selenium Server running:
Set your es and kibana ports in `test/intern.js` to 9220 and 5620, respectively. You can configure your Selenium server to run the tests on Chrome,IE, or other browsers here.
Once you've got the services running, execute the following:
```
sh
npm run test:ui:runner
```
`npm run test:ui:runner`
Execute the front-end browser tests. This requires the server started by the `test:ui:server` task.
#### Browser automation notes:
@ -187,12 +173,12 @@ Packages are built using fpm, pleaserun, dpkg, and rpm. fpm and pleaserun can b
apt-get install ruby-dev rpm
gem install fpm -v 1.5.0 # required by pleaserun 0.0.16
gem install pleaserun -v 0.0.16 # higher versions fail at the moment
npm run build:ospackages
npm run build -- --skip-archives
```
To specify a package to build you can add `rpm` or `deb` as an argument.
```sh
npm run build:ospackages -- --rpm
npm run build -- --rpm
```
Distributable packages can be found in `target/` after the build completes.

View file

@ -9,7 +9,7 @@ module.exports = function (grunt) {
pkg: grunt.file.readJSON('package.json'),
root: __dirname,
src: __dirname + '/src',
build: __dirname + '/build', // temporary build directory
buildDir: __dirname + '/build', // temporary build directory
plugins: __dirname + '/src/plugins',
server: __dirname + '/src/server',
target: __dirname + '/target', // location of the compressed build targets
@ -69,6 +69,9 @@ module.exports = function (grunt) {
grunt.config.merge(config);
// must run before even services/platforms
grunt.config.set('build', require('./tasks/config/build')(grunt));
config.packageScriptsDir = __dirname + '/tasks/build/package_scripts';
// ensure that these run first, other configs need them
config.services = require('./tasks/config/services')(grunt);

View file

@ -1,4 +1,4 @@
# Kibana 5.0.0-snapshot
# Kibana 5.0.0-alpha4
Kibana is an open source ([Apache Licensed](https://github.com/elastic/kibana/blob/master/LICENSE.md)), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elasticsearch.
@ -43,7 +43,7 @@ For the daring, snapshot builds are available. These builds are created after ea
| platform | |
| --- | --- |
| OSX | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-darwin-x64.tar.gz) |
| Linux x64 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x64.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana_5.0.0-snapshot_amd64.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0_snapshot-1.x86_64.rpm) |
| Linux x86 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x86.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana_5.0.0-snapshot_i386.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0_snapshot-1.i386.rpm) |
| Windows | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-windows.zip) |
| OSX | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha4-SNAPSHOT-darwin-x64.tar.gz) |
| Linux x64 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha4-SNAPSHOT-linux-x64.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha4-SNAPSHOT-amd64.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha4-SNAPSHOT-x86_64.rpm) |
| Linux x86 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha4-SNAPSHOT-linux-x86.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha4-SNAPSHOT-i386.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha4-SNAPSHOT-i686.rpm) |
| Windows | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-alpha4-SNAPSHOT-windows.zip) |

View file

@ -8,6 +8,7 @@
:k4pull: https://github.com/elastic/kibana/pull/
:version: master
:esversion: master
:packageversion: master
include::introduction.asciidoc[]

View file

@ -26,7 +26,7 @@ wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add
+
["source","sh",subs="attributes"]
--------------------------------------------------
echo "deb http://packages.elastic.co/kibana/{version}/debian stable main" | sudo tee -a /etc/apt/sources.list.d/kibana.list
echo "deb https://packages.elastic.co/kibana/{packageversion}/debian stable main" | sudo tee -a /etc/apt/sources.list.d/kibana.list
--------------------------------------------------
+
[WARNING]
@ -82,11 +82,11 @@ rpm --import https://packages.elastic.co/GPG-KEY-elasticsearch
+
["source","sh",subs="attributes"]
--------------------------------------------------
[kibana-{version}]
name=Kibana repository for {version}.x packages
baseurl=http://packages.elastic.co/kibana/{version}/centos
[kibana-{packageversion}]
name=Kibana repository for {packageversion} packages
baseurl=https://packages.elastic.co/kibana/{packageversion}/centos
gpgcheck=1
gpgkey=http://packages.elastic.co/GPG-KEY-elasticsearch
gpgkey=https://packages.elastic.co/GPG-KEY-elasticsearch
enabled=1
--------------------------------------------------
+

View file

@ -11,7 +11,7 @@
"dashboarding"
],
"private": false,
"version": "5.0.0-snapshot",
"version": "5.0.0-alpha4",
"build": {
"number": 8467,
"sha": "6cb7fec4e154faa0a4a3fee4b33dfef91b9870d9"
@ -30,6 +30,7 @@
"Jon Budzenski <jonathan.budzenski@elastic.co>",
"Juan Thomassie <juan.thomassie@elastic.co>",
"Khalah Jones-Golden <khalah.jones@elastic.co>",
"Lee Drengenberg <lee.drengenberg@elastic.co>",
"Lukas Olson <lukas.olson@elastic.co>",
"Matt Bargar <matt.bargar@elastic.co>",
"Nicolás Bevacqua <nico@elastic.co>",
@ -47,8 +48,9 @@
"test:ui:runner": "grunt test:ui:runner",
"test:server": "grunt test:server",
"test:coverage": "grunt test:coverage",
"test:visualRegression": "grunt test:visualRegression",
"build": "grunt build",
"build:ospackages": "grunt build --os-packages",
"release": "grunt release",
"start": "sh ./bin/kibana --dev",
"precommit": "grunt precommit",
"karma": "karma start",
@ -59,8 +61,7 @@
"makelogs": "makelogs",
"mocha": "mocha",
"mocha:debug": "mocha --debug-brk",
"sterilize": "grunt sterilize",
"compareScreenshots": "node utilities/compareScreenshots"
"sterilize": "grunt sterilize"
},
"repository": {
"type": "git",
@ -68,7 +69,8 @@
},
"dependencies": {
"@bigfunger/decompress-zip": "0.2.0-stripfix2",
"@elastic/datemath": "2.2.0",
"@bigfunger/jsondiffpatch": "0.1.38-webpack",
"@elastic/datemath": "2.3.0",
"@spalger/angular-bootstrap": "0.12.1",
"@spalger/filesaver": "1.1.2",
"@spalger/leaflet-draw": "0.2.3",
@ -76,6 +78,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 +98,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 +114,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",
@ -127,9 +132,10 @@
"marked": "0.3.3",
"minimatch": "2.0.10",
"mkdirp": "0.5.1",
"moment": "2.10.6",
"moment-timezone": "0.4.1",
"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",
@ -139,6 +145,8 @@
"semver": "5.1.0",
"style-loader": "0.12.3",
"tar": "2.2.0",
"trunc-html": "1.0.2",
"trunc-text": "1.0.2",
"url-loader": "0.5.6",
"validate-npm-package-name": "2.2.2",
"webpack": "1.12.1",
@ -153,6 +161,7 @@
"auto-release-sinon": "1.0.3",
"babel-eslint": "4.1.8",
"chokidar": "1.4.3",
"chromedriver": "2.21.2",
"elasticdump": "2.1.1",
"eslint": "1.10.3",
"eslint-plugin-mocha": "1.1.0",
@ -170,10 +179,11 @@
"grunt-s3": "0.2.0-alpha.3",
"grunt-simple-mocha": "0.4.0",
"gruntify-eslint": "1.0.1",
"handlebars": "4.0.5",
"html-entities": "1.1.3",
"husky": "0.8.1",
"image-diff": "1.6.0",
"intern": "3.0.1",
"intern": "3.2.3",
"istanbul-instrumenter-loader": "0.1.3",
"karma": "0.13.9",
"karma-chrome-launcher": "0.2.0",
@ -184,14 +194,14 @@
"karma-safari-launcher": "0.1.1",
"license-checker": "3.1.0",
"load-grunt-config": "0.19.1",
"makelogs": "3.0.0-beta3",
"makelogs": "3.0.0",
"marked-text-renderer": "0.1.0",
"mocha": "2.3.0",
"ncp": "2.0.0",
"nock": "2.10.0",
"npm": "2.11.0",
"npm": "2.15.1",
"portscanner": "1.0.0",
"simple-git": "1.8.0",
"simple-git": "1.37.0",
"sinon": "1.17.2",
"source-map": "0.4.4",
"source-map-support": "0.4.0",

View file

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

View file

@ -23,6 +23,8 @@ function setLoggingJson(enabled) {
describe(`Server logging configuration`, function () {
it(`should be reloadable via SIGHUP process signaling`, function (done) {
this.timeout(60000);
let asserted = false;
let json = Infinity;
const conf = setLoggingJson(true);
@ -66,7 +68,7 @@ ${err.stack || err.message || err}`).to.eql(true);
}
function switchToPlainTextLog() {
json = 2; // ignore both "reloading" messages
json = 3; // ignore both "reloading" messages + ui settings status message
setLoggingJson(false);
child.kill(`SIGHUP`); // reload logging config
}

View file

@ -3,6 +3,7 @@ import { statSync } from 'fs';
import { isWorker } from 'cluster';
import { resolve } from 'path';
import { fromRoot } from '../../utils';
import { getConfig } from '../../server/path';
import readYamlConfig from './read_yaml_config';
let canCluster;
@ -77,7 +78,7 @@ module.exports = function (program) {
'Path to the config file, can be changed with the CONFIG_PATH environment variable as well. ' +
'Use mulitple --config args to include multiple config files.',
configPathCollector,
[ process.env.CONFIG_PATH || fromRoot('config/kibana.yml') ]
[ getConfig() ]
)
.option('-p, --port <port>', 'The port to bind to', parseInt)
.option('-q, --quiet', 'Prevent all logging except errors')

View file

@ -44,8 +44,8 @@ describe('kibana cli', function () {
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
plugin: 'test-plugin',
version: '5.0.0-snapshot',
plugins: [ { name: 'foo', path: join(testWorkingPath, 'foo'), version: '5.0.0-snapshot' } ]
version: '5.0.0-SNAPSHOT',
plugins: [ { name: 'foo', path: join(testWorkingPath, 'foo'), version: '5.0.0-SNAPSHOT' } ]
};
const errorStub = sinon.stub();

View file

@ -1,8 +1,11 @@
import { fromRoot } from '../../utils';
import fs from 'fs';
import install from './install';
import Logger from '../lib/logger';
import pkg from '../../utils/package_json';
import { getConfig } from '../../server/path';
import { parse, parseMilliseconds } from './settings';
import { find } from 'lodash';
function processCommand(command, options) {
let settings;
@ -26,7 +29,7 @@ export default function pluginInstall(program) {
.option(
'-c, --config <path>',
'path to the config file',
fromRoot('config/kibana.yml')
getConfig()
)
.option(
'-t, --timeout <duration>',

View file

@ -2,6 +2,7 @@ import { fromRoot } from '../../utils';
import remove from './remove';
import Logger from '../lib/logger';
import { parse } from './settings';
import { getConfig } from '../../server/path';
function processCommand(command, options) {
let settings;
@ -25,7 +26,7 @@ export default function pluginRemove(program) {
.option(
'-c, --config <path>',
'path to the config file',
fromRoot('config/kibana.yml')
getConfig()
)
.option(
'-d, --plugin-dir <path>',

View file

@ -13,7 +13,7 @@
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.1-snapshot",
"_id": "4.0.1-SNAPSHOT",
"_score": 1,
"_source": {
"buildNum": 5921,

View file

@ -19,13 +19,11 @@ module.exports = async (kbnServer, server, config) => {
server.exposeStaticDir('/bundles/{path*}', bundles.env.workingDir);
await bundles.writeEntryFiles();
// in prod, only bundle what looks invalid or missing
if (config.get('optimize.useBundleCache')) {
bundles = await bundles.getInvalidBundles();
}
// in prod, only bundle when someing is missing or invalid
let invalidBundles = config.get('optimize.useBundleCache') ? await bundles.getInvalidBundles() : bundles;
// we might not have any work to do
if (!bundles.getIds().length) {
if (!invalidBundles.getIds().length) {
server.log(
['debug', 'optimize'],
`All bundles are cached and ready to go!`

View file

@ -39,8 +39,8 @@ describe('plugins/elasticsearch', function () {
upgradeDoc('4.0.0-rc2', '4.0.2', true);
upgradeDoc('4.0.1', '4.1.0-rc', true);
upgradeDoc('4.0.0-rc1', '4.0.0', true);
upgradeDoc('4.0.0-rc1-snapshot', '4.0.0', false);
upgradeDoc('4.1.0-rc1-snapshot', '4.1.0-rc1', false);
upgradeDoc('4.0.0-rc1-SNAPSHOT', '4.0.0', false);
upgradeDoc('4.1.0-rc1-SNAPSHOT', '4.1.0-rc1', false);
upgradeDoc('5.0.0-alpha1', '5.0.0', false);
it('should handle missing _id field', function () {

View file

@ -94,7 +94,7 @@ describe('plugins/elasticsearch', function () {
it('should create new config if the nothing is upgradeable', function () {
get.withArgs('pkg.buildNum').returns(9833);
client.create.returns(Promise.resolve());
const response = { hits: { hits: [ { _id: '4.0.1-alpha3' }, { _id: '4.0.1-beta1' }, { _id: '4.0.0-snapshot1' } ] } };
const response = { hits: { hits: [ { _id: '4.0.1-alpha3' }, { _id: '4.0.1-beta1' }, { _id: '4.0.0-SNAPSHOT1' } ] } };
return upgrade(response).then(function (resp) {
sinon.assert.calledOnce(client.create);
const params = client.create.args[0][0];

View file

@ -21,9 +21,9 @@ const versionChecks = [
['2.0.1', '^2.0.0', true],
['2.1.1', '^2.1.0', true],
['2.2.0', '^2.1.0', true],
['3.0.0-snapshot', '^2.1.0', false],
['3.0.0-SNAPSHOT', '^2.1.0', false],
['3.0.0', '^2.1.0', false],
['2.10.20-snapshot', '^2.10.20', true],
['2.10.20-SNAPSHOT', '^2.10.20', true],
['2.10.999', '^2.10.20', true],
];

View file

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

View file

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

View file

@ -19,14 +19,13 @@ module.exports = function (kibana) {
title: 'Kibana',
listed: false,
description: 'the kibana you know and love',
//icon: 'plugins/kibana/settings/sections/about/barcode.svg',
main: 'plugins/kibana/kibana',
uses: [
'visTypes',
'spyModes',
'fieldFormats',
'navbarExtensions',
'settingsSections',
'managementSections',
'docViews'
],
@ -62,11 +61,12 @@ module.exports = function (kibana) {
icon: 'plugins/kibana/assets/dashboard.svg',
},
{
title: 'Settings',
title: 'Management',
order: 1000,
url: '/app/kibana#/settings',
url: '/app/kibana#/management',
description: 'define index patterns, change config, and more',
icon: 'plugins/kibana/assets/settings.svg',
linkToLastSubUrl: false
}
],
injectDefaultVars(server, options) {

View file

@ -15,7 +15,7 @@ uiModules
const filterManager = Private(FilterManagerProvider);
const notify = new Notifier();
const services = require('plugins/kibana/settings/saved_object_registry').all().map(function (serviceObj) {
const services = require('plugins/kibana/management/saved_object_registry').all().map(function (serviceObj) {
const service = $injector.get(serviceObj.service);
return {
type: service.type,
@ -79,7 +79,7 @@ uiModules
const service = _.find(services, { type: type });
if (!service) return;
$scope.editUrl = '#settings/objects/' + service.name + '/' + id + '?notFound=' + e.savedObjectType;
$scope.editUrl = '#management/kibana/objects/' + service.name + '/' + id + '?notFound=' + e.savedObjectType;
});
});

View file

@ -18,11 +18,8 @@ import uiRoutes from 'ui/routes';
import uiModules from 'ui/modules';
import indexTemplate from 'plugins/kibana/dashboard/index.html';
require('ui/saved_objects/saved_object_registry').register(require('plugins/kibana/dashboard/services/saved_dashboard_register'));
const app = uiModules.get('app/dashboard', [
'elasticsearch',
'ngRoute',
@ -33,6 +30,9 @@ const app = uiModules.get('app/dashboard', [
]);
uiRoutes
.defaults(/dashboard/, {
requireDefaultIndex: true
})
.when('/dashboard', {
template: indexTemplate,
resolve: {

View file

@ -9,7 +9,7 @@ const module = uiModules.get('app/dashboard');
// Register this service with the saved object registry so it can be
// edited by the object editor.
require('plugins/kibana/settings/saved_object_registry').register({
require('plugins/kibana/management/saved_object_registry').register({
service: 'savedDashboards',
title: 'dashboards'
});

View file

@ -36,6 +36,9 @@ const app = uiModules.get('apps/discover', [
]);
uiRoutes
.defaults(/discover/, {
requireDefaultIndex: true
})
.when('/discover/:id?', {
template: indexTemplate,
reloadOnSearch: false,
@ -65,7 +68,7 @@ uiRoutes
return savedSearches.get($route.current.params.id)
.catch(courier.redirectWhenMissing({
'search': '/discover',
'index-pattern': '/settings/objects/savedSearches/' + $route.current.params.id
'index-pattern': '/management/kibana/objects/savedSearches/' + $route.current.params.id
}));
}
}

View file

@ -11,7 +11,7 @@ const module = uiModules.get('discover/saved_searches', [
// Register this service with the saved object registry so it can be
// edited by the object editor.
require('plugins/kibana/settings/saved_object_registry').register({
require('plugins/kibana/management/saved_object_registry').register({
service: 'savedSearches',
title: 'searches'
});

View file

@ -4,19 +4,17 @@
overflow-x: hidden;
}
.discover {
&-wrapper {
.discover-wrapper {
padding-right: 0px !important;
padding-left: @collapser-width;
}
&-content {
.discover-content {
padding-right: @padding-base-horizontal;
clear: both;
}
&-timechart {
.discover-timechart {
display: block;
position: relative;
@ -56,7 +54,7 @@
}
}
&-overlay {
.discover-overlay {
position: absolute;
top: 0;
left: 0;
@ -74,19 +72,18 @@
}
}
&-info {
.discover-info {
line-height: 30px;
padding: 0px 10px;
border-bottom-left-radius: @border-radius-base;
}
&-title {
.discover-info-title {
font-weight: bold;
margin-right: 10px;
}
}
&-table {
.discover-table {
overflow-y: auto;
overflow-x: auto;
padding-left: 0px !important;
@ -95,45 +92,41 @@
tbody {
font-family: "Lucida Console", Monaco, monospace;
}
}
&-footer {
.discover-table-footer {
background-color: @discover-table-footer-bg;
padding: 5px 10px;
}
&-details-toggle {
.discover-table-details-toggle {
margin-bottom: 3px;
}
&-timefield {
.discover-table-timefield {
white-space: nowrap;
}
&-open-icon {
.discover-table-open-icon {
// when switching between open and closed, the toggle changes size
// slightly which is a problem because it forces the entire table to
// re-render which is SLOW
width: 7px;
}
}
&-field {
&-filter {
.discover-field-filter {
background-color: @discover-field-filter-bg;
margin-right: 10px;
.form-group {
margin-bottom: 0px;
}
}
&-toggle {
.discover-field-toggle {
color: @discover-field-toggle-color;
font-size: 9px;
}
}
}
.shard-failures {
color: @discover-shard-failures-color;
@ -166,8 +159,9 @@
padding: 5px 10px;
background-color: @discover-field-details-bg;
color: @discover-field-details-color;
}
&-close {
.discover-field-details-close {
text-align: center;
border-top: 1px solid;
border-color: @discover-field-details-close-border;
@ -179,22 +173,26 @@
}
}
&-count {
.discover-field-details-count {
white-space: nowrap;
}
&-error {
.discover-field-details-error {
margin-top: 5px;
}
&-item {
.discover-field-details-item {
margin-top: 5px;
}
&-filter {
.discover-field-details-filter {
cursor: pointer;
}
/**
* TODO: Refactor these selectors to be less specific.
*/
.discover-field-details {
a {
color: @discover-link-color !important;
}
@ -261,8 +259,9 @@ disc-field-chooser {
margin: -20px 0 10px 0;
text-align: center;
}
}
&-interval {
.results-interval {
a {
text-decoration: underline;
}
@ -272,4 +271,3 @@ disc-field-chooser {
width: auto;
}
}
}

View file

@ -11,7 +11,7 @@ import 'ui/autoload/all';
import 'plugins/kibana/discover/index';
import 'plugins/kibana/visualize/index';
import 'plugins/kibana/dashboard/index';
import 'plugins/kibana/settings/index';
import 'plugins/kibana/management/index';
import 'plugins/kibana/doc';
import 'ui/vislib';
import 'ui/agg_response';

View file

@ -0,0 +1,23 @@
<div class="app-container">
<nav class="navbar navbar-default navbar-static-top subnav" data-test-subj="managementNav">
<bread-crumbs omit-current-page="true"></bread-crumbs>
<ul class="nav navbar-nav">
<li class="current-page" ng-hide="sectionName">
{{::section.display}}
</li>
<li
ng-if="sectionName"
ng-repeat="item in section.items.inOrder"
ng-class="item.class">
<a class="navbar-link" kbn-href="{{::item.url}}" data-test-subj="{{::item.name}}">
{{::item.display}}
</a>
</li>
</ul>
</nav>
<div role="main" class="management-container" ng-transclude></div>
</div>

View file

@ -0,0 +1,64 @@
import _ from 'lodash';
import 'plugins/kibana/management/sections';
import 'plugins/kibana/management/styles/main.less';
import 'ui/filters/start_from';
import 'ui/field_editor';
import 'plugins/kibana/management/sections/indices/_indexed_fields';
import 'plugins/kibana/management/sections/indices/_scripted_fields';
import 'plugins/kibana/management/sections/indices/field_filters/field_filters';
import 'ui/directives/bread_crumbs';
import uiRoutes from 'ui/routes';
import uiModules from 'ui/modules';
import appTemplate from 'plugins/kibana/management/app.html';
import landingTemplate from 'plugins/kibana/management/landing.html';
import chrome from 'ui/chrome/chrome';
import management from 'ui/management';
uiRoutes
.when('/management', {
template: landingTemplate
});
require('ui/index_patterns/route_setup/load_default')({
whenMissingRedirectTo: '/management/data/index'
});
uiModules
.get('apps/management')
.directive('kbnManagementApp', function (Private, $route, $location, timefilter, buildNum, buildSha) {
return {
restrict: 'E',
template: appTemplate,
transclude: true,
scope: {
sectionName: '@section'
},
link: function ($scope) {
timefilter.enabled = false;
$scope.sections = management.items.inOrder;
$scope.section = management.getSection($scope.sectionName) || management;
if ($scope.section) {
$scope.section.items.forEach(item => {
item.class = `#${$location.path()}`.indexOf(item.url) > -1 ? 'active' : undefined;
});
}
management.getSection('kibana').info = `Build ${buildNum}, Commit SHA ${buildSha.substr(0, 8)}`;
}
};
});
uiModules
.get('apps/management')
.directive('kbnManagementLanding', function (kbnVersion) {
return {
restrict: 'E',
link: function ($scope) {
$scope.sections = management.items.inOrder;
$scope.kbnVersion = kbnVersion;
}
};
});

View file

@ -0,0 +1,45 @@
<kbn-management-app>
<kbn-management-landing>
<div class="product-overview">
<span class="kibana-version">Version: {{::kbnVersion}}</span>
</div>
<div class="management-sections">
<div
ng-if="section.items.length > 0"
ng-repeat="section in sections"
ng-class="{ 'management-section-info-expanded': section.showInfo }"
class="col-xs-12 management-section management-section-{{::section.id}}">
<div class="panel panel-product management-panel-product">
<div class="panel-heading panel-heading-{{::section.id}}">
{{::section.display}}
<i
class="fa fa-info-circle pull-right panel-heading-icon"
ng-click="section.showInfo = !!!section.showInfo"
ng-if="section.info">
</i>
</div>
<div class="panel-body">
<div class="row">
<ul class="management-section-items list-unstyled">
<li
class="col-xs-4 col-md-3"
ng-repeat="item in section.items.inOrder">
<a class="management-link" kbn-href="{{::item.url}}">
{{::item.display}}
</a>
</li>
</ul>
</div>
<div class="management-section-info">
{{::section.info}}
</div>
</div>
</div>
</div>
</div>
</kbn-management-landing>
</kbn-management-app>

View file

@ -0,0 +1,3 @@
import 'plugins/kibana/management/sections/settings';
import 'plugins/kibana/management/sections/objects';
import 'plugins/kibana/management/sections/indices';

View file

@ -1,6 +1,6 @@
<kbn-settings-app section="indices">
<kbn-settings-indices>
<div ng-controller="settingsIndicesCreate" class="kbn-settings-indices-create">
<kbn-management-app section="data">
<kbn-management-indices>
<div ng-controller="managementIndicesCreate" class="kbn-management-indices-create">
<div class="page-header">
<h1>Configure an index pattern</h1>
In order to use Kibana you must configure at least one index pattern. Index patterns are
@ -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"
@ -177,5 +179,5 @@
</form>
</div>
</div>
</kbn-settings-indices>
</kbn-settings-app>
</kbn-management-indices>
</kbn-management-app>

View file

@ -3,21 +3,20 @@ import moment from 'moment';
import { IndexPatternMissingIndices } from 'ui/errors';
import 'ui/directives/validate_index_name';
import 'ui/directives/auto_select_if_only_one';
import PluginsKibanaSettingsSectionsIndicesRefreshKibanaIndexProvider from 'plugins/kibana/settings/sections/indices/_refresh_kibana_index';
import RefreshKibanaIndex from 'plugins/kibana/management/sections/indices/_refresh_kibana_index';
import uiRoutes from 'ui/routes';
import uiModules from 'ui/modules';
import createTemplate from 'plugins/kibana/settings/sections/indices/_create.html';
import createTemplate from 'plugins/kibana/management/sections/indices/_create.html';
uiRoutes
.when('/settings/indices/', {
.when('/management/data/index/', {
template: createTemplate
});
uiModules.get('apps/settings')
.controller('settingsIndicesCreate', function ($scope, kbnUrl, Private, Notifier, indexPatterns, es, config, Promise) {
uiModules.get('apps/management')
.controller('managementIndicesCreate', function ($scope, kbnUrl, Private, Notifier, indexPatterns, es, config, Promise) {
const notify = new Notifier();
const refreshKibanaIndex = Private(PluginsKibanaSettingsSectionsIndicesRefreshKibanaIndexProvider);
const refreshKibanaIndex = Private(RefreshKibanaIndex);
const intervals = indexPatterns.intervals;
let samplePromise;
@ -73,7 +72,7 @@ uiModules.get('apps/settings')
config.set('defaultIndex', indexPattern.id);
}
indexPatterns.cache.clear(indexPattern.id);
kbnUrl.change('/settings/indices/' + indexPattern.id);
kbnUrl.change('/management/kibana/indices/' + indexPattern.id);
});
}
});

View file

@ -1,13 +1,13 @@
<kbn-settings-app section="indices">
<kbn-settings-indices>
<div ng-controller="settingsIndicesEdit">
<kbn-management-app section="kibana">
<kbn-management-indices>
<div ng-controller="managementIndicesEdit" data-test-subj="editIndexPattern">
<div class="page-header">
<kbn-settings-index-header
<kbn-management-index-header
index-pattern="indexPattern"
set-default="setDefaultPattern()"
refresh-fields="indexPattern.refreshFields()"
delete="removePattern()">
</kbn-settings-index-header>
</kbn-management-index-header>
<p>
This page lists every field in the <strong>{{::indexPattern.id}}</strong>
@ -39,29 +39,29 @@
<!-- tab list -->
<ul class="nav nav-tabs">
<li class="kbn-settings-tab" ng-repeat="tab in tabs" ng-class="{ active: state.tab === tab.index }">
<a ng-click="changeTab(tab)">
{{ tab.title }}
<li class="kbn-management-tab" ng-class="{ active: state.tab === fieldType.index }" ng-repeat="fieldType in fieldTypes">
<a ng-click="changeTab(fieldType)">
{{ fieldType.title }}
<small ng-if="tab.count">({{ tab.count(indexPattern) }})</small>
</a>
</li>
</ul>
<!-- tabs -->
<settings-indices-indexed-fields
<indexed-fields
ng-show="state.tab == 'indexedFields'"
class="fields indexed-fields">
</settings-indices-indexed-fields>
<settings-indices-scripted-fields
</indexed-fields>
<scripted-fields
ng-show="state.tab == 'scriptedFields'"
class="fields scripted-fields">
</settings-indices-scripted-fields>
<settings-indices-field-filters
</scripted-fields>
<field-filters
ng-show="state.tab == 'fieldFilters'"
index-pattern="indexPattern"
class="fields field-filters">
</settings-indices-field-filters>
</field-filters>
</div>
</kbn-settings-indices>
</kbn-settings-app>
</kbn-management-indices>
</kbn-management-app>

View file

@ -0,0 +1,94 @@
import _ from 'lodash';
import 'plugins/kibana/management/sections/indices/_indexed_fields';
import 'plugins/kibana/management/sections/indices/_scripted_fields';
import 'plugins/kibana/management/sections/indices/field_filters/field_filters';
import 'plugins/kibana/management/sections/indices/_index_header';
import RefreshKibanaIndex from 'plugins/kibana/management/sections/indices/_refresh_kibana_index';
import UrlProvider from 'ui/url';
import IndicesFieldTypesProvider from 'plugins/kibana/management/sections/indices/_field_types';
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?', {
template: editTemplate,
resolve: {
indexPattern: function ($route, config, courier) {
const params = $route.current.params;
if (typeof params.indexPatternId === 'undefined') {
params.indexPatternId = config.get('defaultIndex');
}
return courier.indexPatterns.get(params.indexPatternId)
.catch(courier.redirectWhenMissing('/management/data/index'));
}
}
});
uiModules.get('apps/management')
.controller('managementIndicesEdit', function ($scope, $location, $route, config, courier, Notifier, Private, AppState, docTitle) {
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;
docTitle.change($scope.indexPattern.id);
const otherIds = _.without($route.current.locals.indexPatternIds, $scope.indexPattern.id);
const fieldTypes = Private(IndicesFieldTypesProvider);
$scope.$watch('indexPattern.fields', function () {
$scope.fieldTypes = fieldTypes($scope.indexPattern);
});
$scope.changeTab = function (obj) {
$state.tab = obj.index;
$state.save();
};
$scope.$watch('state.tab', function (tab) {
if (!tab) $scope.changeTab($scope.fieldTypes[0]);
});
$scope.$watchCollection('indexPattern.fields', function (fields) {
$scope.conflictFields = _.filter(fields, { type: 'conflict' });
});
$scope.refreshFields = function () {
$scope.indexPattern.refreshFields();
};
$scope.removePattern = function () {
if ($scope.indexPattern.id === config.get('defaultIndex')) {
config.remove('defaultIndex');
if (otherIds.length) {
config.set('defaultIndex', otherIds[0]);
}
}
ingest.delete($scope.indexPattern.id)
.then($scope.indexPattern.destroy.bind($scope.indexPattern))
.then(function () {
$location.url('/management/data/index');
})
.catch(notify.fatal);
};
$scope.setDefaultPattern = function () {
config.set('defaultIndex', $scope.indexPattern.id);
};
$scope.setIndexPatternsTimeField = function (field) {
if (field.type !== 'date') {
notify.error('That field is a ' + field.type + ' not a date.');
return;
}
$scope.indexPattern.timeFieldName = field.name;
return $scope.indexPattern.save();
};
});

View file

@ -1,7 +1,7 @@
<kbn-settings-app section="indices">
<kbn-settings-indices>
<kbn-management-app section="kibana">
<kbn-management-indices>
<div class="page-header">
<kbn-settings-index-header index-pattern="fieldSettings.indexPattern"></kbn-settings-index-header>
<kbn-management-index-header index-pattern="fieldSettings.indexPattern"></kbn-management-index-header>
<h2 ng-if="fieldSettings.mode === 'create'">
Create {{ fieldSettings.field.scripted ? 'Scripted ' : '' }}Field
@ -14,5 +14,5 @@
<field-editor index-pattern="fieldSettings.indexPattern" field="fieldSettings.field"></field-editor>
</kbn-settings-indices>
</kbn-settings-app>
</kbn-management-indices>
</kbn-management-app>

View file

@ -1,19 +1,19 @@
import 'ui/field_editor';
import 'plugins/kibana/settings/sections/indices/_index_header';
import 'plugins/kibana/management/sections/indices/_index_header';
import IndexPatternsFieldProvider from 'ui/index_patterns/_field';
import UrlProvider from 'ui/url';
import uiRoutes from 'ui/routes';
import fieldEditorTemplate from 'plugins/kibana/settings/sections/indices/_field_editor.html';
import fieldEditorTemplate from 'plugins/kibana/management/sections/indices/_field_editor.html';
uiRoutes
.when('/settings/indices/:indexPatternId/field/:fieldName', { mode: 'edit' })
.when('/settings/indices/:indexPatternId/create-field/', { mode: 'create' })
.defaults(/settings\/indices\/[^\/]+\/(field|create-field)(\/|$)/, {
.when('/management/kibana/indices/:indexPatternId/field/:fieldName', { mode: 'edit' })
.when('/management/kibana/indices/:indexPatternId/create-field/', { mode: 'create' })
.defaults(/management\/kibana\/indices\/[^\/]+\/(field|create-field)(\/|$)/, {
template: fieldEditorTemplate,
resolve: {
indexPattern: function ($route, courier) {
return courier.indexPatterns.get($route.current.params.indexPatternId)
.catch(courier.redirectWhenMissing('/settings/indices'));
.catch(courier.redirectWhenMissing('/management/kibana/indices'));
}
},
controllerAs: 'fieldSettings',
@ -22,7 +22,6 @@ uiRoutes
const notify = new Notifier({ location: 'Field Editor' });
const kbnUrl = Private(UrlProvider);
this.mode = $route.current.mode;
this.indexPattern = $route.current.locals.indexPattern;
@ -54,4 +53,3 @@ uiRoutes
};
}
});

View file

@ -1,8 +1,8 @@
import uiModules from 'ui/modules';
import indexHeaderTemplate from 'plugins/kibana/settings/sections/indices/_index_header.html';
import indexHeaderTemplate from 'plugins/kibana/management/sections/indices/_index_header.html';
uiModules
.get('apps/settings')
.directive('kbnSettingsIndexHeader', function (config) {
.get('apps/management')
.directive('kbnManagementIndexHeader', function (config) {
return {
restrict: 'E',
template: indexHeaderTemplate,

View file

@ -1,14 +1,14 @@
import _ from 'lodash';
import 'ui/paginated_table';
import nameHtml from 'plugins/kibana/settings/sections/indices/_field_name.html';
import typeHtml from 'plugins/kibana/settings/sections/indices/_field_type.html';
import controlsHtml from 'plugins/kibana/settings/sections/indices/_field_controls.html';
import nameHtml from 'plugins/kibana/management/sections/indices/_field_name.html';
import typeHtml from 'plugins/kibana/management/sections/indices/_field_type.html';
import controlsHtml from 'plugins/kibana/management/sections/indices/_field_controls.html';
import uiModules from 'ui/modules';
import indexedFieldsTemplate from 'plugins/kibana/settings/sections/indices/_indexed_fields.html';
import FieldWildcardProvider from 'ui/field_wildcard';
import indexedFieldsTemplate from 'plugins/kibana/management/sections/indices/_indexed_fields.html';
uiModules.get('apps/settings')
.directive('settingsIndicesIndexedFields', function (Private, $filter) {
uiModules.get('apps/management')
.directive('indexedFields', function (Private, $filter) {
const yesTemplate = '<i class="fa fa-check" aria-label="yes"></i>';
const noTemplate = '';
const filter = $filter('filter');

View file

@ -1,13 +1,13 @@
import _ from 'lodash';
import 'ui/paginated_table';
import popularityHtml from 'plugins/kibana/settings/sections/indices/_field_popularity.html';
import controlsHtml from 'plugins/kibana/settings/sections/indices/_field_controls.html';
import dateScripts from 'plugins/kibana/settings/sections/indices/_date_scripts';
import popularityHtml from 'plugins/kibana/management/sections/indices/_field_popularity.html';
import controlsHtml from 'plugins/kibana/management/sections/indices/_field_controls.html';
import dateScripts from 'plugins/kibana/management/sections/indices/_date_scripts';
import uiModules from 'ui/modules';
import scriptedFieldsTemplate from 'plugins/kibana/settings/sections/indices/_scripted_fields.html';
import scriptedFieldsTemplate from 'plugins/kibana/management/sections/indices/_scripted_fields.html';
uiModules.get('apps/settings')
.directive('settingsIndicesScriptedFields', function (kbnUrl, Notifier, $filter) {
uiModules.get('apps/management')
.directive('scriptedFields', function (kbnUrl, Notifier, $filter) {
const rowScopes = []; // track row scopes, so they can be destroyed as needed
const filter = $filter('filter');
@ -19,7 +19,7 @@ uiModules.get('apps/settings')
scope: true,
link: function ($scope) {
const fieldCreatorPath = '/settings/indices/{{ indexPattern }}/scriptedField';
const fieldCreatorPath = '/management/kibana/indices/{{ indexPattern }}/scriptedField';
const fieldEditorPath = fieldCreatorPath + '/{{ fieldName }}';
$scope.perPage = 25;
@ -37,6 +37,8 @@ uiModules.get('apps/settings')
rowScopes.length = 0;
const fields = filter($scope.indexPattern.getScriptedFields(), $scope.fieldFilter);
_.find($scope.fieldTypes, {index: 'scriptedFields'}).count = fields.length; // Update the tab count
$scope.rows = fields.map(function (field) {
const rowScope = $scope.$new();
rowScope.field = field;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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