mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Merge remote-tracking branch 'upstream/master' into console_scripts
This commit is contained in:
commit
795fcc0cdd
453 changed files with 7894 additions and 21276 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -26,7 +26,8 @@ target
|
|||
data
|
||||
disabledPlugins
|
||||
webpackstats.json
|
||||
config/kibana.dev.yml
|
||||
config/*
|
||||
!config/kibana.yml
|
||||
coverage
|
||||
selenium
|
||||
.babelcache.json
|
||||
|
|
18
Gruntfile.js
18
Gruntfile.js
|
@ -49,23 +49,7 @@ module.exports = function (grunt) {
|
|||
'!<%= src %>/core_plugins/timelion/vendor_components/**/*.js',
|
||||
'!<%= src %>/fixtures/**/*.js',
|
||||
'!<%= root %>/test/fixtures/scenarios/**/*.js'
|
||||
],
|
||||
deepModules: {
|
||||
'caniuse-db': '1.0.30000265',
|
||||
'chalk': '1.1.0',
|
||||
'glob': '4.5.3',
|
||||
'har-validator': '1.8.0',
|
||||
'json5': '0.4.0',
|
||||
'loader-utils': '0.2.11',
|
||||
'micromatch': '2.2.0',
|
||||
'postcss-normalize-url': '2.1.1',
|
||||
'postcss-reduce-idents': '1.0.2',
|
||||
'postcss-unique-selectors': '1.0.0',
|
||||
'postcss-minify-selectors': '1.4.6',
|
||||
'postcss-single-charset': '0.3.0',
|
||||
'regenerator': '0.8.36',
|
||||
'readable-stream': '2.1.0'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
grunt.config.merge(config);
|
||||
|
|
91
README.md
91
README.md
|
@ -1,26 +1,59 @@
|
|||
# Kibana 6.0.0-alpha1
|
||||
|
||||
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.
|
||||
Kibana is your window into the [Elastic Stack](https://www.elastic.co/products). Specifically, it's
|
||||
an open source ([Apache Licensed](LICENSE.md)),
|
||||
browser-based analytics and search dashboard for Elasticsearch.
|
||||
|
||||
## Requirements
|
||||
- [Getting Started](#getting-started)
|
||||
- [Using a Kibana Release](#using-a-kibana-release)
|
||||
- [Building and Running Kibana, and/or Contributing Code](#building-and-running-kibana-andor-contributing-code)
|
||||
- [Snapshot Builds](#snapshot-builds)
|
||||
- [Documentation](#documentation)
|
||||
- [Version Compatibility with Elasticsearch](#version-compatibility-with-elasticsearch)
|
||||
- [Questions? Problems? Suggestions?](#questions-problems-suggestions)
|
||||
|
||||
- Elasticsearch master
|
||||
- Kibana binary package
|
||||
## Getting Started
|
||||
|
||||
## Installation
|
||||
If you just want to try Kibana out, check out the [Elastic Stack Getting Started Page](https://www.elastic.co/start) to give it a whirl.
|
||||
|
||||
* Download: [http://www.elastic.co/downloads/kibana](http://www.elastic.co/downloads/kibana)
|
||||
* Extract the files
|
||||
* Run `bin/kibana` on unix, or `bin\kibana.bat` on Windows.
|
||||
* Visit [http://localhost:5601](http://localhost:5601)
|
||||
If you're interested in diving a bit deeper and getting a taste of Kibana's capabilities, head over to the [Kibana Getting Started Page](https://www.elastic.co/guide/en/kibana/current/getting-started.html).
|
||||
|
||||
## Upgrade from previous version
|
||||
### Using a Kibana Release
|
||||
|
||||
* Move any custom configurations in your old kibana.yml to your new one
|
||||
* Reinstall plugins
|
||||
* Start or restart Kibana
|
||||
If you want to use a Kibana release in production, give it a test run, or just play around:
|
||||
|
||||
## Version compatibility with Elasticsearch
|
||||
- Download the latest version on the [Kibana Download Page](https://www.elastic.co/downloads/kibana).
|
||||
- Learn more about Kibana's features and capabilities on the
|
||||
[Kibana Product Page](https://www.elastic.co/products/kibana).
|
||||
- We also offer a hosted version of Kibana on our
|
||||
[Cloud Service](https://www.elastic.co/cloud/as-a-service).
|
||||
|
||||
### Building and Running Kibana, and/or Contributing Code
|
||||
|
||||
You may want to build Kibana locally to contribute some code, test out the latest features, or try
|
||||
out an open PR:
|
||||
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) will help you get Kibana up and running.
|
||||
- If you would like to contribute code, please follow our [STYLEGUIDE.md](STYLEGUIDE.md).
|
||||
- For all other questions, check out the [FAQ.md](FAQ.md) and
|
||||
[wiki](https://github.com/elastic/kibana/wiki).
|
||||
|
||||
### Snapshot Builds
|
||||
|
||||
For the daring, snapshot builds are available. These builds are created after each commit to the master branch, and therefore are not something you should run in production.
|
||||
|
||||
| platform | |
|
||||
| --- | --- |
|
||||
| OSX | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-darwin-x86_64.tar.gz) |
|
||||
| Linux x64 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-linux-x86_64.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-amd64.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-x86_64.rpm) |
|
||||
| Linux x86 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-linux-x86.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-i386.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-i686.rpm) |
|
||||
| Windows | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-windows-x86.zip) |
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit [Elastic.co](http://www.elastic.co/guide/en/kibana/current/index.html) for the full Kibana documentation.
|
||||
|
||||
## Version Compatibility with Elasticsearch
|
||||
|
||||
Ideally, you should be running Elasticsearch and Kibana with matching version numbers. If your Elasticsearch has an older version number or a newer _major_ number than Kibana, then Kibana will fail to run. If Elasticsearch has a newer minor or patch number than Kibana, then the Kibana Server will log a warning.
|
||||
|
||||
|
@ -36,29 +69,9 @@ _Note: The version numbers below are only examples, meant to illustrate the rela
|
|||
| ES minor number is older. | 5.__1__.2 | 5.__0__.0 | 🚫 Fatal error |
|
||||
| ES major number is older. | __5__.1.2 | __4__.0.0 | 🚫 Fatal error |
|
||||
|
||||
## Quick Start
|
||||
## Questions? Problems? Suggestions?
|
||||
|
||||
You're up and running! Fantastic! Kibana is now running on port 5601, so point your browser at http://YOURDOMAIN.com:5601.
|
||||
|
||||
The first screen you arrive at will ask you to configure an **index pattern**. An index pattern describes to Kibana how to access your data. We make the guess that you're working with log data, and we hope (because it's awesome) that you're working with Logstash. By default, we fill in `logstash-*` as your index pattern, thus the only thing you need to do is select which field contains the timestamp you'd like to use. Kibana reads your Elasticsearch mapping to find your time fields - select one from the list and hit *Create*.
|
||||
|
||||
Congratulations, you have an index pattern! You should now be looking at a paginated list of the fields in your index or indices, as well as some informative data about them. Kibana has automatically set this new index pattern as your default index pattern. If you'd like to know more about index patterns, pop into to the [Settings](#settings) section of the documentation.
|
||||
|
||||
**Did you know:** Both *indices* and *indexes* are acceptable plural forms of the word *index*. Knowledge is power.
|
||||
|
||||
Now that you've configured an index pattern, you're ready to hop over to the [Discover](#discover) screen and try out a few searches. Click on **Discover** in the navigation bar at the top of the screen.
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit [Elastic.co](http://www.elastic.co/guide/en/kibana/current/index.html) for the full Kibana documentation.
|
||||
|
||||
## Snapshot Builds
|
||||
|
||||
For the daring, snapshot builds are available. These builds are created after each commit to the master branch, and therefore are not something you should run in production.
|
||||
|
||||
| platform | |
|
||||
| --- | --- |
|
||||
| OSX | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-darwin-x86_64.tar.gz) |
|
||||
| Linux x64 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-linux-x86_64.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-amd64.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-x86_64.rpm) |
|
||||
| Linux x86 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-linux-x86.tar.gz) [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-i386.deb) [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-i686.rpm) |
|
||||
| Windows | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-6.0.0-alpha1-SNAPSHOT-windows-x86.zip) |
|
||||
- If you've found a bug or want to request a feature, please create a [GitHub Issue](https://github.com/elastic/kibana/issues/new).
|
||||
Please check to make sure someone else hasn't already created an issue for the same topic.
|
||||
- Need help using Kibana? Ask away on our [Kibana Discuss Forum](https://discuss.elastic.co/c/kibana) and a fellow community member or
|
||||
Elastic engineer will be glad to help you out.
|
|
@ -21,4 +21,4 @@ if [ ! -x "$NODE" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
exec "${NODE}" $NODE_OPTIONS --no-warnings "${DIR}/src/cli_plugin" ${@}
|
||||
exec "${NODE}" $NODE_OPTIONS --no-warnings "${DIR}/src/cli_plugin" "$@"
|
||||
|
|
|
@ -20,13 +20,13 @@
|
|||
# The URL of the Elasticsearch instance to use for all your queries.
|
||||
#elasticsearch.url: "http://localhost:9200"
|
||||
|
||||
# When this setting’s value is true Kibana uses the hostname specified in the server.host
|
||||
# When this setting's value is true Kibana uses the hostname specified in the server.host
|
||||
# setting. When the value of this setting is false, Kibana uses the hostname of the host
|
||||
# that connects to this Kibana instance.
|
||||
#elasticsearch.preserveHost: true
|
||||
|
||||
# Kibana uses an index in Elasticsearch to store saved searches, visualizations and
|
||||
# dashboards. Kibana creates a new index if the index doesn’t already exist.
|
||||
# dashboards. Kibana creates a new index if the index doesn't already exist.
|
||||
#kibana.index: ".kibana"
|
||||
|
||||
# The default application to load.
|
||||
|
@ -53,7 +53,7 @@
|
|||
# authority for your Elasticsearch instance.
|
||||
#elasticsearch.ssl.ca: /path/to/your/CA.pem
|
||||
|
||||
# To disregard the validity of SSL certificates, change this setting’s value to false.
|
||||
# To disregard the validity of SSL certificates, change this setting's value to false.
|
||||
#elasticsearch.ssl.verify: true
|
||||
|
||||
# Time in milliseconds to wait for Elasticsearch to respond to pings. Defaults to the value of
|
||||
|
|
|
@ -83,4 +83,4 @@ include::console/history.asciidoc[]
|
|||
|
||||
include::console/settings.asciidoc[]
|
||||
|
||||
include::console/disabling-console.asciidoc[]
|
||||
include::console/configuring-console.asciidoc[]
|
||||
|
|
57
docs/console/configuring-console.asciidoc
Normal file
57
docs/console/configuring-console.asciidoc
Normal file
|
@ -0,0 +1,57 @@
|
|||
[[configuring-console]]
|
||||
== Configuring Console
|
||||
|
||||
You can add the following options in the `config/kibana.yml` file:
|
||||
|
||||
`console.enabled`:: *Default: true* Set to false to disable Console. Toggling this will cause the server to regenerate assets on the next startup, which may cause a delay before pages start being served.
|
||||
|
||||
`console.proxyFilter`:: *Default: `.*`* A list of regular expressions that are used to validate any outgoing request from Console. If none
|
||||
of these match, the request will be rejected. See <<securing-console>> for more details.
|
||||
|
||||
`console.proxyConfig`:: A list of configuration options that are based on the proxy target. Use this to set custom timeouts or SSL settings for specific hosts. This is done by defining a set of `match` criteria using wildcards/globs which will be checked against each request. The configuration from all matching rules will then be merged together to configure the proxy used for that request.
|
||||
+
|
||||
The valid match keys are `match.protocol`, `match.host`, `match.port`, and `match.path`. All of these keys default to `*`, which means they will match any value.
|
||||
+
|
||||
Example:
|
||||
+
|
||||
[source,yaml]
|
||||
--------
|
||||
console.proxyConfig:
|
||||
- match:
|
||||
host: "*.internal.org" # allow any host that ends in .internal.org
|
||||
port: "{9200..9299}" # allow any port from 9200-9299
|
||||
|
||||
ssl:
|
||||
ca: "/opt/certs/internal.ca"
|
||||
# "key" and "cert" are also valid options here
|
||||
|
||||
- match:
|
||||
protocol: "https"
|
||||
|
||||
ssl:
|
||||
verify: false # allows any certificate to be used, even self-signed certs
|
||||
|
||||
# since this rule has no "match" section it matches everything
|
||||
- timeout: 180000 # 3 minutes
|
||||
--------
|
||||
|
||||
[[securing-console]]
|
||||
=== Securing Console
|
||||
|
||||
Console is meant to be used as a local development tool. As such, it will send requests to any host & port combination,
|
||||
just as a local curl command would. To overcome the CORS limitations enforced by browsers, Console's Node.js backend
|
||||
serves as a proxy to send requests on behalf of the browser. However, if put on a server and exposed to the internet
|
||||
this can become a security risk. In those cases, we highly recommend you lock down the proxy by setting the
|
||||
`console.proxyFilter` setting. The setting accepts a list of regular expressions that are evaluated against each URL
|
||||
the proxy is requested to retrieve. If none of the regular expressions match the proxy will reject the request.
|
||||
|
||||
Here is an example configuration the only allows Console to connect to localhost:
|
||||
|
||||
[source,yaml]
|
||||
--------
|
||||
console.proxyFilter:
|
||||
- ^https?://(localhost|127\.0\.0\.1|\[::0\]).*
|
||||
--------
|
||||
|
||||
You will need to restart Kibana for these changes to take effect.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
[[disabling-console]]
|
||||
== Disable Console
|
||||
|
||||
If the users of Kibana have no requirements or need to access any of the Console functionality, it can
|
||||
be disabled completely and not even show up as an available app by setting the `console.enabled` Kibana server setting to `false`:
|
||||
|
||||
[source,yaml]
|
||||
--------
|
||||
console.enabled: false
|
||||
--------
|
|
@ -133,7 +133,7 @@ to open *Management/Kibana/Saved Objects/Dashboards*.
|
|||
[[sharing-dashboards]]
|
||||
== Sharing a Dashboard
|
||||
|
||||
You can can share a direct link to a Kibana dashboard with another user,
|
||||
You can either share a direct link to a Kibana dashboard with another user,
|
||||
or embed the dashboard in a web page. Users must have Kibana access
|
||||
to view embedded dashboards.
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
:version: 6.0.0-alpha1
|
||||
:major-version: 6.x
|
||||
:docker-image: docker.elastic.co/kibana/kibana:{version}
|
||||
|
||||
//////////
|
||||
release-state can be: released | prerelease | unreleased
|
||||
|
@ -13,6 +14,8 @@ release-state can be: released | prerelease | unreleased
|
|||
:xpack-ref: https://www.elastic.co/guide/en/x-pack/current/
|
||||
:issue: https://github.com/elastic/kibana/issues/
|
||||
:pull: https://github.com/elastic/kibana/pull/
|
||||
:commit: https://github.com/elastic/kibana/commit/
|
||||
:security: https://www.elastic.co/community/security/
|
||||
|
||||
|
||||
include::introduction.asciidoc[]
|
||||
|
|
|
@ -6,6 +6,14 @@
|
|||
Add-on functionality for Kibana is implemented with plug-in modules. You can use the `bin/kibana-plugin`
|
||||
command to manage these modules. You can also install a plugin manually by moving the plugin file to the
|
||||
`plugins` directory and unpacking the plugin files into a new directory.
|
||||
|
||||
[IMPORTANT]
|
||||
.Plugin compatibility
|
||||
==============================================
|
||||
|
||||
The Kibana plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. Kibana enforces that the installed plugins match the version of Kibana itself. Plugin developers will have to release a new version of their plugin for each new Kibana release as a result.
|
||||
|
||||
==============================================
|
||||
--
|
||||
|
||||
== Installing Plugins
|
||||
|
|
|
@ -50,6 +50,8 @@ include::setup/install.asciidoc[]
|
|||
|
||||
include::setup/settings.asciidoc[]
|
||||
|
||||
include::setup/docker.asciidoc[]
|
||||
|
||||
include::setup/access.asciidoc[]
|
||||
|
||||
include::setup/connect-to-elasticsearch.asciidoc[]
|
||||
|
|
|
@ -32,8 +32,8 @@ sophisticated date parsing APIs that Kibana uses to determine date information,
|
|||
specify dates in the index pattern name.
|
||||
+
|
||||
. Click *Create* to add the index pattern. This first pattern is automatically configured as the default.
|
||||
When you have more than one index pattern, you can designate which one to use as the default from
|
||||
*Settings > Indices*.
|
||||
When you have more than one index pattern, you can designate which one to use as the default by clicking
|
||||
on the star icon above the index pattern title from *Management > Index Patterns*.
|
||||
|
||||
All done! Kibana is now connected to your Elasticsearch data. Kibana displays a read-only list of fields
|
||||
configured for the matching index.
|
||||
|
|
115
docs/setup/docker.asciidoc
Normal file
115
docs/setup/docker.asciidoc
Normal file
|
@ -0,0 +1,115 @@
|
|||
[[docker]]
|
||||
== Running Kibana on Docker
|
||||
Docker images for Kibana are available from the Elastic Docker registry.
|
||||
The images are shipped with https://www.elastic.co/products/x-pack[X-Pack]
|
||||
installed.
|
||||
|
||||
NOTE: https://www.elastic.co/guide/en/x-pack/current/index.html[X-Pack] is
|
||||
pre-installed in this image. With X-Pack installed, Kibana expects to
|
||||
connect to an Elasticsearch cluster that is also runnning X-Pack.
|
||||
|
||||
=== Pulling the image
|
||||
Obtaining Kibana for Docker is as simple as issuing a +docker pull+ command
|
||||
against the Elastic Docker registry.
|
||||
|
||||
ifeval::["{release-state}"=="unreleased"]
|
||||
|
||||
However, version {version} of Kibana has not yet been released, so no Docker
|
||||
image is currently available for this version.
|
||||
|
||||
endif::[]
|
||||
|
||||
ifeval::["{release-state}"!="unreleased"]
|
||||
|
||||
The Docker image for Kibana {version} can be retrieved with the following
|
||||
command:
|
||||
|
||||
["source","sh",subs="attributes"]
|
||||
--------------------------------------------
|
||||
docker pull {docker-image}
|
||||
--------------------------------------------
|
||||
|
||||
endif::[]
|
||||
|
||||
=== Configuring Kibana on Docker
|
||||
|
||||
The Docker image provides several methods for configuring Kibana. The conventional
|
||||
approach is to provide a `kibana.yml` file as described in <<settings>>, but it's
|
||||
also possible to use environment variables to define settings.
|
||||
|
||||
==== Bind-mounted configuration
|
||||
|
||||
One way to configure Kibana on Docker is to provide `kibana.yml` via bind-mounting.
|
||||
With +docker-compose+, the bind-mount can be specified like this:
|
||||
|
||||
["source","yaml",subs="attributes"]
|
||||
--------------------------------------------
|
||||
services:
|
||||
kibana:
|
||||
image: {docker-image}
|
||||
volumes:
|
||||
- ./kibana.yml:/usr/share/kibana/config/kibana.yml
|
||||
--------------------------------------------
|
||||
|
||||
==== Environment variable configuration
|
||||
|
||||
Under Docker, Kibana can be configured via environment variables. The following
|
||||
mappings are available:
|
||||
|
||||
.Docker Environment Variables
|
||||
[horizontal]
|
||||
**Environment Variable**:: **Kibana Setting**
|
||||
`ELASTICSEARCH_CUSTOMHEADERS`:: `elasticsearch.customHeaders`
|
||||
`ELASTICSEARCH_PASSWORD`:: `elasticsearch.password`
|
||||
`ELASTICSEARCH_PINGTIMEOUT`:: `elasticsearch.pingTimeout`
|
||||
`ELASTICSEARCH_PRESERVEHOST`:: `elasticsearch.preserveHost`
|
||||
`ELASTICSEARCH_REQUESTHEADERSWHITELIST`:: `elasticsearch.requestHeadersWhitelist`
|
||||
`ELASTICSEARCH_REQUESTTIMEOUT`:: `elasticsearch.requestTimeout`
|
||||
`ELASTICSEARCH_SHARDTIMEOUT`:: `elasticsearch.shardTimeout`
|
||||
`ELASTICSEARCH_SSL_CA`:: `elasticsearch.ssl.ca`
|
||||
`ELASTICSEARCH_SSL_CERT`:: `elasticsearch.ssl.cert`
|
||||
`ELASTICSEARCH_SSL_KEY`:: `elasticsearch.ssl.key`
|
||||
`ELASTICSEARCH_SSL_VERIFY`:: `elasticsearch.ssl.verify`
|
||||
`ELASTICSEARCH_STARTUPTIMEOUT`:: `elasticsearch.startupTimeout`
|
||||
`ELASTICSEARCH_URL`:: `elasticsearch.url`
|
||||
`ELASTICSEARCH_USERNAME`:: `elasticsearch.username`
|
||||
`KIBANA_DEFAULTAPPID`:: `kibana.defaultAppId`
|
||||
`KIBANA_INDEX`:: `kibana.index`
|
||||
`LOGGING_DEST`:: `logging.dest`
|
||||
`LOGGING_QUIET`:: `logging.quiet`
|
||||
`LOGGING_SILENT`:: `logging.silent`
|
||||
`LOGGING_VERBOSE`:: `logging.verbose`
|
||||
`OPS_INTERVAL`:: `ops.interval`
|
||||
`PID_FILE`:: `pid.file`
|
||||
`SERVER_BASEPATH`:: `server.basePath`
|
||||
`SERVER_HOST`:: `server.host`
|
||||
`SERVER_MAXPAYLOADBYTES`:: `server.maxPayloadBytes`
|
||||
`SERVER_NAME`:: `server.name`
|
||||
`SERVER_PORT`:: `server.port`
|
||||
`SERVER_SSL_CERT`:: `server.ssl.cert`
|
||||
`SERVER_SSL_KEY`:: `server.ssl.key`
|
||||
|
||||
These variables can be set with +docker-compose+ like this:
|
||||
|
||||
["source","yaml",subs="attributes"]
|
||||
----------------------------------------------------------
|
||||
services:
|
||||
kibana:
|
||||
image: {docker-image}
|
||||
environment:
|
||||
SERVER_NAME: kibana.example.org
|
||||
ELASTICSEARCH_URL: http://elasticsearch.example.org
|
||||
----------------------------------------------------------
|
||||
|
||||
Environment variables take precedence over settings configured in `kibana.yml`.
|
||||
|
||||
==== Docker defaults
|
||||
The following settings have different default values when using the Docker image:
|
||||
|
||||
[horizontal]
|
||||
`server.host`:: `"0"`
|
||||
`elasticsearch.url`:: `http://elasticsearch:9200`
|
||||
`elasticsearch.username`:: `elastic`
|
||||
`elasticsearch.password`:: `changeme`
|
||||
|
||||
|
|
@ -29,6 +29,15 @@ Elastic website or from our RPM repository.
|
|||
+
|
||||
<<rpm>>
|
||||
|
||||
`docker`::
|
||||
|
||||
An image is available for running Kibana as a Docker container. It ships
|
||||
with https://www.elastic.co/products/x-pack[X-Pack] pre-installed and is
|
||||
available from the Elastic
|
||||
Docker registry.
|
||||
+
|
||||
<<docker>>
|
||||
|
||||
IMPORTANT: If your Elasticsearch installation is protected by {xpack-ref}xpack-security.html[X-Pack Security]
|
||||
see {xpack-ref}kibana.html[Using Kibana with X-Pack Security] for additional setup
|
||||
instructions.
|
||||
|
|
|
@ -79,8 +79,8 @@ ca: /path/to/your/ca/cacert.pem
|
|||
[[load-balancing]]
|
||||
=== Load Balancing Across Multiple Elasticsearch Nodes
|
||||
If you have multiple nodes in your Elasticsearch cluster, the easiest way to distribute Kibana requests
|
||||
across the nodes is to run an Elasticsearch _client_ node on the same machine as Kibana.
|
||||
Elasticsearch client nodes are essentially smart load balancers that are part of the cluster. They
|
||||
across the nodes is to run an Elasticsearch _Coordinating only_ node on the same machine as Kibana.
|
||||
Elasticsearch Coordinating only nodes are essentially smart load balancers that are part of the cluster. They
|
||||
process incoming HTTP requests, redirect operations to the other nodes in the cluster as needed, and
|
||||
gather and return the results. For more information, see
|
||||
{es-ref}modules-node.html[Node] in the Elasticsearch reference.
|
||||
|
@ -88,15 +88,16 @@ gather and return the results. For more information, see
|
|||
To use a local client node to load balance Kibana requests:
|
||||
|
||||
. Install Elasticsearch on the same machine as Kibana.
|
||||
. Configure the node as a client node. In `elasticsearch.yml`, set both `node.data` and `node.master` to `false`:
|
||||
. Configure the node as a Coordinating only node. In `elasticsearch.yml`, set `node.data`, `node.master` and `node.ingest` to `false`:
|
||||
+
|
||||
--------
|
||||
# 3. You want this node to be neither master nor data node, but
|
||||
# 3. You want this node to be neither master nor data node nor ingest node, but
|
||||
# to act as a "search load balancer" (fetching data from nodes,
|
||||
# aggregating results, etc.)
|
||||
#
|
||||
node.master: false
|
||||
node.data: false
|
||||
node.ingest: false
|
||||
--------
|
||||
. Configure the client node to join your Elasticsearch cluster. In `elasticsearch.yml`, set the `cluster.name` to the
|
||||
name of your cluster.
|
||||
|
|
|
@ -14,6 +14,7 @@ you'll need to update your `kibana.yml` file. You can also enable SSL and set a
|
|||
to Kibana. This setting cannot end in a slash (`/`).
|
||||
`server.maxPayloadBytes:`:: *Default: 1048576* The maximum payload size in bytes for incoming server requests.
|
||||
`server.name:`:: *Default: "your-hostname"* A human-readable display name that identifies this Kibana instance.
|
||||
`server.defaultRoute:`:: *Default: "/app/kibana"* This setting specifies the default route when opening Kibana. You can use this setting to modify the landing page when opening Kibana.
|
||||
`elasticsearch.url:`:: *Default: "http://localhost:9200"* The URL of the Elasticsearch instance to use for all your
|
||||
queries.
|
||||
`elasticsearch.preserveHost:`:: *Default: true* When this setting’s value is true Kibana uses the hostname specified in
|
||||
|
@ -63,3 +64,8 @@ information and all requests.
|
|||
The minimum value is 100.
|
||||
`status.allowAnonymous`:: *Default: false* If authentication is enabled, setting this to `true` allows
|
||||
unauthenticated users to access the Kibana server status API and status page.
|
||||
`console.enabled`:: *Default: true* Set to false to disable Console. Toggling this will cause the server to regenerate assets on the next startup, which may cause a delay before pages start being served.
|
||||
`console.proxyFilter`:: *Default: `.*`* A list of regular expressions that are used to validate any outgoing request from Console. If none of these match, the request will be rejected.
|
||||
`console.proxyConfig`:: A list of configuration options that are based on the proxy target. Use this to set custom timeouts or SSL settings for specific hosts. This is done by defining a set of `match` criteria using wildcards/globs which will be checked against each request. The configuration from all matching rules will then be merged together to configure the proxy used for that request.
|
||||
+
|
||||
The valid match keys are `match.protocol`, `match.host`, `match.port`, and `match.path`. All of these keys default to `*`, which means they will match any value. See <<configuring-console>> for an example.
|
||||
|
|
|
@ -33,6 +33,7 @@ different series.
|
|||
instructions.
|
||||
<<metric-chart,Metric>>:: Display a single number.
|
||||
<<pie-chart,Pie chart>>:: Display each source's contribution to a total.
|
||||
<<tagcloud-chart,Tag cloud>>:: Display words as a cloud in which the size of the word correspond to its importance
|
||||
<<tilemap,Tile map>>:: Associate the results of an aggregation with geographic
|
||||
locations.
|
||||
Timeseries:: Compute and combine data from multiple time series
|
||||
|
@ -118,3 +119,5 @@ include::visualize/pie.asciidoc[]
|
|||
include::visualize/tilemap.asciidoc[]
|
||||
|
||||
include::visualize/vertbar.asciidoc[]
|
||||
|
||||
include::visualize/tagcloud.asciidoc[]
|
||||
|
|
44
docs/visualize/tagcloud.asciidoc
Normal file
44
docs/visualize/tagcloud.asciidoc
Normal file
|
@ -0,0 +1,44 @@
|
|||
[[tagcloud-chart]]
|
||||
== Tag Clouds
|
||||
|
||||
A tag cloud visualization is a visual representation of text data, typically used to visualize free form text.
|
||||
Tags are usually single words, and the importance of each tag is shown with font size or color.
|
||||
|
||||
The font size for each word is determined by the _metrics_ aggregation. The following aggregations are available for
|
||||
this chart:
|
||||
|
||||
include::y-axis-aggs.asciidoc[]
|
||||
|
||||
|
||||
The _buckets_ aggregations determine what information is being retrieved from your data set.
|
||||
|
||||
Before you choose a buckets aggregation, select the *Split Tags* option.
|
||||
|
||||
You can specify the following bucket aggregations for tag cloud visualization:
|
||||
|
||||
*Terms*:: A {es-ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top
|
||||
or bottom _n_ elements of a given field to display, ordered by count or a custom metric.
|
||||
|
||||
You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation:
|
||||
|
||||
*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation
|
||||
definition, as in the following example:
|
||||
|
||||
[source,shell]
|
||||
{ "script" : "doc['grade'].value * 1.2" }
|
||||
|
||||
NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable
|
||||
{es-ref}modules-scripting.html[dynamic Groovy scripting].
|
||||
|
||||
|
||||
Select the *Options* tab to change the following aspects of the chart:
|
||||
|
||||
*Text Scale*:: You can select *linear*, *log*, or *square root* scales for the text scale. You can use a log
|
||||
scale to display data that varies exponentially or a square root scale to
|
||||
regularize the display of data sets with variabilities that are themselves highly variable.
|
||||
*Orientation*:: You can select how to orientate your text in the tag cloud. You can choose one of the following options:
|
||||
Single, right angles and multiple.
|
||||
*Font Size*:: Allows you to set minimum and maximum font size to use for this visualization.
|
||||
|
||||
|
||||
include::visualization-raw-data.asciidoc[]
|
|
@ -72,7 +72,7 @@
|
|||
"@bigfunger/decompress-zip": "0.2.0-stripfix3",
|
||||
"@bigfunger/jsondiffpatch": "0.1.38-webpack",
|
||||
"@elastic/datemath": "2.3.0",
|
||||
"@elastic/kibana-ui-framework": "0.0.10",
|
||||
"@elastic/kibana-ui-framework": "0.0.11",
|
||||
"@spalger/filesaver": "1.1.2",
|
||||
"@spalger/leaflet-draw": "0.2.3",
|
||||
"@spalger/leaflet-heat": "0.1.3",
|
||||
|
@ -101,6 +101,7 @@
|
|||
"commander": "2.8.1",
|
||||
"css-loader": "0.17.0",
|
||||
"d3": "3.5.6",
|
||||
"d3-cloud": "1.2.1",
|
||||
"dragula": "3.7.0",
|
||||
"elasticsearch": "12.0.0-rc5",
|
||||
"elasticsearch-browser": "12.0.0-rc5",
|
||||
|
@ -138,6 +139,7 @@
|
|||
"mkdirp": "0.5.1",
|
||||
"moment": "2.13.0",
|
||||
"moment-timezone": "0.5.4",
|
||||
"no-ui-slider": "1.2.0",
|
||||
"node-fetch": "1.3.2",
|
||||
"node-uuid": "1.4.7",
|
||||
"pegjs": "0.9.0",
|
||||
|
@ -168,6 +170,7 @@
|
|||
"auto-release-sinon": "1.0.3",
|
||||
"babel-eslint": "4.1.8",
|
||||
"chai": "3.5.0",
|
||||
"cheerio": "0.22.0",
|
||||
"chokidar": "1.6.0",
|
||||
"chromedriver": "2.24.1",
|
||||
"elasticdump": "2.1.1",
|
||||
|
@ -202,7 +205,7 @@
|
|||
"karma-safari-launcher": "0.1.1",
|
||||
"license-checker": "5.1.2",
|
||||
"load-grunt-config": "0.19.2",
|
||||
"makelogs": "3.0.2",
|
||||
"makelogs": "3.1.1",
|
||||
"marked-text-renderer": "0.1.0",
|
||||
"mocha": "2.5.3",
|
||||
"murmurhash3js": "3.0.1",
|
||||
|
|
72
src/cli_plugin/install/__tests__/rename.js
Normal file
72
src/cli_plugin/install/__tests__/rename.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
import fs from 'fs';
|
||||
|
||||
import { renamePlugin } from '../rename';
|
||||
|
||||
describe('plugin folder rename', function () {
|
||||
let renameStub;
|
||||
|
||||
beforeEach(function () {
|
||||
renameStub = sinon.stub();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
fs.rename.restore();
|
||||
});
|
||||
|
||||
it('should rethrow any exceptions', function () {
|
||||
renameStub = sinon.stub(fs, 'rename', function (from, to, cb) {
|
||||
cb({
|
||||
code: 'error'
|
||||
});
|
||||
});
|
||||
|
||||
return renamePlugin('/foo/bar', '/bar/foo')
|
||||
.catch(function (err) {
|
||||
expect(err.code).to.be('error');
|
||||
expect(renameStub.callCount).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve if there are no errors', function () {
|
||||
renameStub = sinon.stub(fs, 'rename', function (from, to, cb) {
|
||||
cb();
|
||||
});
|
||||
|
||||
return renamePlugin('/foo/bar', '/bar/foo')
|
||||
.then(function (err) {
|
||||
expect(renameStub.callCount).to.be(1);
|
||||
})
|
||||
.catch(function () {
|
||||
throw new Error('We shouln\'t have any errors');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Windows', function () {
|
||||
let platform;
|
||||
beforeEach(function () {
|
||||
platform = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'win32'
|
||||
});
|
||||
});
|
||||
afterEach(function () {
|
||||
Object.defineProperty(process, 'platform', platform);
|
||||
});
|
||||
|
||||
it('should retry on Windows EPERM errors for up to 3 seconds', function () {
|
||||
this.timeout(5000);
|
||||
renameStub = sinon.stub(fs, 'rename', function (from, to, cb) {
|
||||
cb({
|
||||
code: 'EPERM'
|
||||
});
|
||||
});
|
||||
return renamePlugin('/foo/bar', '/bar/foo')
|
||||
.catch(function (err) {
|
||||
expect(err.code).to.be('EPERM');
|
||||
expect(renameStub.callCount).to.be.greaterThan(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,8 +2,8 @@ import { download } from './download';
|
|||
import Promise from 'bluebird';
|
||||
import { cleanPrevious, cleanArtifacts } from './cleanup';
|
||||
import { extract, getPackData } from './pack';
|
||||
import { renamePlugin } from './rename';
|
||||
import { sync as rimrafSync } from 'rimraf';
|
||||
import { renameSync } from 'fs';
|
||||
import { existingInstall, rebuildCache, assertVersion } from './kibana';
|
||||
import mkdirp from 'mkdirp';
|
||||
|
||||
|
@ -27,7 +27,7 @@ export default async function install(settings, logger) {
|
|||
|
||||
assertVersion(settings);
|
||||
|
||||
renameSync(settings.workingPath, settings.plugins[0].path);
|
||||
await renamePlugin(settings.workingPath, settings.plugins[0].path);
|
||||
|
||||
await rebuildCache(settings, logger);
|
||||
|
||||
|
|
21
src/cli_plugin/install/rename.js
Normal file
21
src/cli_plugin/install/rename.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { rename } from 'fs';
|
||||
import { delay } from 'lodash';
|
||||
|
||||
export function renamePlugin(workingPath, finalPath) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const start = Date.now();
|
||||
const retryTime = 3000;
|
||||
const retryDelay = 100;
|
||||
rename(workingPath, finalPath, function retry(err) {
|
||||
if (err) {
|
||||
// In certain cases on Windows, such as running AV, plugin folders can be locked shortly after extracting
|
||||
// Retry for up to retryTime seconds
|
||||
const windowsEPERM = process.platform === 'win32' && err.code === 'EPERM';
|
||||
const retryAvailable = Date.now() - start < retryTime;
|
||||
if (windowsEPERM && retryAvailable) return delay(rename, retryDelay, workingPath, finalPath, retry);
|
||||
reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -56,6 +56,12 @@ describe('plugins/elasticsearch', () => {
|
|||
client.nodes.info = sinon.stub().returns(Promise.resolve({ nodes: nodes }));
|
||||
}
|
||||
|
||||
function setNodeWithoutHTTP(version) {
|
||||
const nodes = { 'node-without-http': { version, ip: 'ip' } };
|
||||
const client = server.plugins.elasticsearch.client;
|
||||
client.nodes.info = sinon.stub().returns(Promise.resolve({ nodes: nodes }));
|
||||
}
|
||||
|
||||
it('returns true with single a node that matches', async () => {
|
||||
setNodes('5.1.0');
|
||||
const result = await checkEsVersion(server, KIBANA_VERSION);
|
||||
|
@ -99,6 +105,24 @@ describe('plugins/elasticsearch', () => {
|
|||
expect(server.log.getCall(1).args[0]).to.contain('warning');
|
||||
});
|
||||
|
||||
it('warns if a node is off by a patch version and without http publish address', async () => {
|
||||
setNodeWithoutHTTP('5.1.1');
|
||||
await checkEsVersion(server, KIBANA_VERSION);
|
||||
sinon.assert.callCount(server.log, 2);
|
||||
expect(server.log.getCall(0).args[0]).to.contain('debug');
|
||||
expect(server.log.getCall(1).args[0]).to.contain('warning');
|
||||
});
|
||||
|
||||
it('errors if a node incompatible and without http publish address', async () => {
|
||||
setNodeWithoutHTTP('6.1.1');
|
||||
try {
|
||||
await checkEsVersion(server, KIBANA_VERSION);
|
||||
} catch (e) {
|
||||
expect(e.message).to.contain('incompatible nodes');
|
||||
expect(e).to.be.a(Error);
|
||||
}
|
||||
});
|
||||
|
||||
it('only warns once per node list', async () => {
|
||||
setNodes('5.1.1');
|
||||
|
||||
|
|
|
@ -53,9 +53,7 @@ describe('plugins/elasticsearch', function () {
|
|||
expect(params.body.mappings.config.properties)
|
||||
.to.have.property('buildNum');
|
||||
expect(params.body.mappings.config.properties.buildNum)
|
||||
.to.have.property('type', 'string');
|
||||
expect(params.body.mappings.config.properties.buildNum)
|
||||
.to.have.property('index', 'not_analyzed');
|
||||
.to.have.property('type', 'keyword');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import getBasicAuthRealm from '../get_basic_auth_realm';
|
||||
import expect from 'expect.js';
|
||||
const exception = '[security_exception] missing authentication token for REST request [/logstash-*/_search],' +
|
||||
' with: {"header":{"WWW-Authenticate":"Basic realm=\\"shield\\""}}';
|
||||
|
||||
|
||||
describe('plugins/elasticsearch', function () {
|
||||
describe('lib/get_basic_auth_realm', function () {
|
||||
|
||||
it('should return null if passed something other than a string', function () {
|
||||
expect(getBasicAuthRealm({})).to.be(null);
|
||||
expect(getBasicAuthRealm(500)).to.be(null);
|
||||
expect(getBasicAuthRealm([exception])).to.be(null);
|
||||
});
|
||||
|
||||
// TODO: This should be updated to match header strings when the client supports that
|
||||
it('should return the realm when passed an elasticsearch security exception', function () {
|
||||
expect(getBasicAuthRealm(exception)).to.be('shield');
|
||||
});
|
||||
|
||||
it('should return null when no basic realm information is found', function () {
|
||||
expect(getBasicAuthRealm('Basically nothing="the universe"')).to.be(null);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import _ from 'lodash';
|
||||
import Promise from 'bluebird';
|
||||
import Boom from 'boom';
|
||||
import getBasicAuthRealm from './get_basic_auth_realm';
|
||||
import toPath from 'lodash/internal/toPath';
|
||||
import filterHeaders from './filter_headers';
|
||||
|
||||
module.exports = (server, client) => {
|
||||
return (req, endpoint, params = {}) => {
|
||||
return (req, endpoint, clientParams = {}, options = {}) => {
|
||||
const wrap401Errors = options.wrap401Errors !== false;
|
||||
const filteredHeaders = filterHeaders(req.headers, server.config().get('elasticsearch.requestHeadersWhitelist'));
|
||||
_.set(params, 'headers', filteredHeaders);
|
||||
_.set(clientParams, 'headers', filteredHeaders);
|
||||
const path = toPath(endpoint);
|
||||
const api = _.get(client, path);
|
||||
let apiContext = _.get(client, path.slice(0, -1));
|
||||
|
@ -16,16 +16,16 @@ module.exports = (server, client) => {
|
|||
apiContext = client;
|
||||
}
|
||||
if (!api) throw new Error(`callWithRequest called with an invalid endpoint: ${endpoint}`);
|
||||
return api.call(apiContext, params)
|
||||
return api.call(apiContext, clientParams)
|
||||
.catch((err) => {
|
||||
if (err.status === 401) {
|
||||
// TODO: The err.message is temporary until we have support for getting headers in the client.
|
||||
// Once we have that, we should be able to pass the contents of the WWW-Authenticate head to getRealm
|
||||
const realm = getBasicAuthRealm(err.message) || 'Authorization Required';
|
||||
const options = { realm: realm };
|
||||
return Promise.reject(Boom.unauthorized('Unauthorized', 'Basic', options));
|
||||
if (!wrap401Errors || err.statusCode !== 401) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return Promise.reject(err);
|
||||
|
||||
const boomError = Boom.wrap(err, err.statusCode);
|
||||
const wwwAuthHeader = _.get(err, 'body.error.header[WWW-Authenticate]');
|
||||
boomError.output.headers['WWW-Authenticate'] = wwwAuthHeader || 'Basic realm="Authorization Required"';
|
||||
throw boomError;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -44,7 +44,8 @@ module.exports = function checkEsVersion(server, kibanaVersion) {
|
|||
|
||||
function getHumanizedNodeNames(nodes) {
|
||||
return nodes.map(node => {
|
||||
return 'v' + node.version + ' @ ' + node.http.publish_address + ' (' + node.ip + ')';
|
||||
const publishAddress = _.get(node, 'http.publish_address') ? (_.get(node, 'http.publish_address') + ' ') : '';
|
||||
return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')';
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -52,7 +53,7 @@ module.exports = function checkEsVersion(server, kibanaVersion) {
|
|||
const simplifiedNodes = warningNodes.map(node => ({
|
||||
version: node.version,
|
||||
http: {
|
||||
publish_address: node.http.publish_address,
|
||||
publish_address: _.get(node, 'http.publish_address')
|
||||
},
|
||||
ip: node.ip,
|
||||
}));
|
||||
|
@ -78,7 +79,7 @@ module.exports = function checkEsVersion(server, kibanaVersion) {
|
|||
throw new Error(
|
||||
`This version of Kibana requires Elasticsearch v` +
|
||||
`${kibanaVersion} on all nodes. I found ` +
|
||||
`the following incompatible nodes in your cluster: ${incompatibleNodeNames.join(',')}`
|
||||
`the following incompatible nodes in your cluster: ${incompatibleNodeNames.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
export default function getBasicAuthRealm(message) {
|
||||
if (!message || typeof message !== 'string') return null;
|
||||
|
||||
const parts = message.match(/Basic\ realm=\\"(.*)\\"/);
|
||||
if (parts && parts.length === 2) return parts[1];
|
||||
else return null;
|
||||
};
|
|
@ -81,13 +81,20 @@ module.exports = function (plugin, server) {
|
|||
});
|
||||
}
|
||||
|
||||
function waitForEsVersion() {
|
||||
return checkEsVersion(server, kibanaVersion.get()).catch(err => {
|
||||
plugin.status.red(err);
|
||||
return Promise.delay(REQUEST_DELAY).then(waitForEsVersion);
|
||||
});
|
||||
}
|
||||
|
||||
function setGreenStatus() {
|
||||
return plugin.status.green('Kibana index ready');
|
||||
}
|
||||
|
||||
function check() {
|
||||
return waitForPong()
|
||||
.then(() => checkEsVersion(server, kibanaVersion.get()))
|
||||
.then(waitForEsVersion)
|
||||
.then(waitForShards)
|
||||
.then(setGreenStatus)
|
||||
.then(_.partial(migrateConfig, server))
|
||||
|
|
|
@ -2,8 +2,7 @@ export const mappings = {
|
|||
config: {
|
||||
properties: {
|
||||
buildNum: {
|
||||
type: 'string',
|
||||
index: 'not_analyzed'
|
||||
type: 'keyword'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -16,7 +16,6 @@ export default function HistogramVisType(Private) {
|
|||
'effect on the series above it.',
|
||||
params: {
|
||||
defaults: {
|
||||
shareYAxis: true,
|
||||
addTooltip: true,
|
||||
addLegend: true,
|
||||
legendPosition: 'right',
|
||||
|
@ -27,8 +26,7 @@ export default function HistogramVisType(Private) {
|
|||
times: [],
|
||||
addTimeMarker: false,
|
||||
defaultYExtents: false,
|
||||
setYExtents: false,
|
||||
yAxis: {}
|
||||
setYExtents: false
|
||||
},
|
||||
legendPositions: [{
|
||||
value: 'left',
|
||||
|
@ -47,6 +45,7 @@ export default function HistogramVisType(Private) {
|
|||
modes: ['stacked', 'overlap', 'percentage', 'wiggle', 'silhouette'],
|
||||
editor: areaTemplate
|
||||
},
|
||||
implementsRenderComplete: true,
|
||||
schemas: new Schemas([
|
||||
{
|
||||
group: 'metrics',
|
||||
|
|
|
@ -14,7 +14,6 @@ export default function HistogramVisType(Private) {
|
|||
'exact numbers or percentages. If you are not sure which chart you need, you could do worse than to start here.',
|
||||
params: {
|
||||
defaults: {
|
||||
shareYAxis: true,
|
||||
addTooltip: true,
|
||||
addLegend: true,
|
||||
legendPosition: 'right',
|
||||
|
@ -23,8 +22,7 @@ export default function HistogramVisType(Private) {
|
|||
times: [],
|
||||
addTimeMarker: false,
|
||||
defaultYExtents: false,
|
||||
setYExtents: false,
|
||||
yAxis: {}
|
||||
setYExtents: false
|
||||
},
|
||||
legendPositions: [{
|
||||
value: 'left',
|
||||
|
@ -43,6 +41,7 @@ export default function HistogramVisType(Private) {
|
|||
modes: ['stacked', 'percentage', 'grouped'],
|
||||
editor: histogramTemplate
|
||||
},
|
||||
implementsRenderComplete: true,
|
||||
schemas: new Schemas([
|
||||
{
|
||||
group: 'metrics',
|
||||
|
|
|
@ -14,7 +14,6 @@ export default function HistogramVisType(Private) {
|
|||
'Be careful with sparse sets as the connection between points can be misleading.',
|
||||
params: {
|
||||
defaults: {
|
||||
shareYAxis: true,
|
||||
addTooltip: true,
|
||||
addLegend: true,
|
||||
legendPosition: 'right',
|
||||
|
@ -27,8 +26,7 @@ export default function HistogramVisType(Private) {
|
|||
times: [],
|
||||
addTimeMarker: false,
|
||||
defaultYExtents: false,
|
||||
setYExtents: false,
|
||||
yAxis: {}
|
||||
setYExtents: false
|
||||
},
|
||||
legendPositions: [{
|
||||
value: 'left',
|
||||
|
@ -46,6 +44,7 @@ export default function HistogramVisType(Private) {
|
|||
scales: ['linear', 'log', 'square root'],
|
||||
editor: lineTemplate
|
||||
},
|
||||
implementsRenderComplete: true,
|
||||
schemas: new Schemas([
|
||||
{
|
||||
group: 'metrics',
|
||||
|
|
|
@ -14,7 +14,6 @@ export default function HistogramVisType(Private) {
|
|||
'Pro Tip: Pie charts are best used sparingly, and with no more than 7 slices per pie.',
|
||||
params: {
|
||||
defaults: {
|
||||
shareYAxis: true,
|
||||
addTooltip: true,
|
||||
addLegend: true,
|
||||
legendPosition: 'right',
|
||||
|
@ -37,6 +36,7 @@ export default function HistogramVisType(Private) {
|
|||
},
|
||||
responseConverter: false,
|
||||
hierarchicalData: true,
|
||||
implementsRenderComplete: true,
|
||||
schemas: new Schemas([
|
||||
{
|
||||
group: 'metrics',
|
||||
|
|
|
@ -81,6 +81,7 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
|
|||
}
|
||||
},
|
||||
responseConverter: geoJsonConverter,
|
||||
implementsRenderComplete: true,
|
||||
schemas: new Schemas([
|
||||
{
|
||||
group: 'metrics',
|
||||
|
|
|
@ -2,12 +2,13 @@ import angular from 'angular';
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import 'plugins/kibana/dashboard/services/_saved_dashboard';
|
||||
import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../components/panel/lib/panel_state';
|
||||
|
||||
describe('dashboard panels', function () {
|
||||
let $scope;
|
||||
let $el;
|
||||
|
||||
const compile = (dashboard) => {
|
||||
function compile(dashboard) {
|
||||
ngMock.inject(($rootScope, $controller, $compile, $route) => {
|
||||
$scope = $rootScope.$new();
|
||||
$route.current = {
|
||||
|
@ -19,12 +20,16 @@ describe('dashboard panels', function () {
|
|||
$el = angular.element(`
|
||||
<dashboard-app>
|
||||
<dashboard-grid style="width: 600px; height: 600px;"></dashboard-grid>
|
||||
</<dashboard-app>`);
|
||||
</dashboard-app>`);
|
||||
$compile($el)($scope);
|
||||
$scope.$digest();
|
||||
});
|
||||
};
|
||||
|
||||
function findPanelWithVisualizationId(id) {
|
||||
return $scope.state.panels.find((panel) => { return panel.id === id; });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
@ -77,10 +82,30 @@ describe('dashboard panels', function () {
|
|||
compile(dash);
|
||||
});
|
||||
expect($scope.state.panels.length).to.be(16);
|
||||
const foo8Panel = $scope.state.panels.find(
|
||||
(panel) => { return panel.id === 'foo8'; });
|
||||
const foo8Panel = findPanelWithVisualizationId('foo8');
|
||||
expect(foo8Panel).to.not.be(null);
|
||||
expect(foo8Panel.row).to.be(8);
|
||||
expect(foo8Panel.col).to.be(1);
|
||||
});
|
||||
|
||||
it('initializes visualizations with the default size', function () {
|
||||
ngMock.inject((SavedDashboard) => {
|
||||
let dash = new SavedDashboard();
|
||||
dash.init();
|
||||
dash.panelsJSON = `[
|
||||
{"col":3,"id":"foo1","row":1,"type":"visualization"},
|
||||
{"col":5,"id":"foo2","row":1,"size_x":5,"size_y":9,"type":"visualization"}]`;
|
||||
compile(dash);
|
||||
});
|
||||
expect($scope.state.panels.length).to.be(2);
|
||||
const foo1Panel = findPanelWithVisualizationId('foo1');
|
||||
expect(foo1Panel).to.not.be(null);
|
||||
expect(foo1Panel.size_x).to.be(DEFAULT_PANEL_WIDTH);
|
||||
expect(foo1Panel.size_y).to.be(DEFAULT_PANEL_HEIGHT);
|
||||
|
||||
const foo2Panel = findPanelWithVisualizationId('foo2');
|
||||
expect(foo2Panel).to.not.be(null);
|
||||
expect(foo2Panel.size_x).to.be(5);
|
||||
expect(foo2Panel.size_y).to.be(9);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import _ from 'lodash';
|
||||
import PluginsKibanaDashboardComponentsPanelLibVisualizationProvider from 'plugins/kibana/dashboard/components/panel/lib/visualization';
|
||||
import PluginsKibanaDashboardComponentsPanelLibSearchProvider from 'plugins/kibana/dashboard/components/panel/lib/search';
|
||||
export default function loadPanelFunction(Private) { // Inject services here
|
||||
import { visualizationLoaderProvider } from 'plugins/kibana/dashboard/components/panel/lib/visualization';
|
||||
import { searchLoaderProvider } from 'plugins/kibana/dashboard/components/panel/lib/search';
|
||||
|
||||
export function loadPanelProvider(Private) { // Inject services here
|
||||
return function (panel, $scope) { // Function parameters here
|
||||
const panelTypes = {
|
||||
visualization: Private(PluginsKibanaDashboardComponentsPanelLibVisualizationProvider),
|
||||
search: Private(PluginsKibanaDashboardComponentsPanelLibSearchProvider)
|
||||
visualization: Private(visualizationLoaderProvider),
|
||||
search: Private(searchLoaderProvider)
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
export const DEFAULT_PANEL_WIDTH = 3;
|
||||
export const DEFAULT_PANEL_HEIGHT = 2;
|
||||
|
||||
/**
|
||||
* Represents a panel on a grid. Keeps track of position in the grid and what visualization it
|
||||
* contains.
|
||||
*
|
||||
* @typedef {Object} PanelState
|
||||
* @property {number} id - Id of the visualization contained in the panel.
|
||||
* @property {Element} $el - A reference to the gridster widget holding this panel. Used to
|
||||
* update the size and column attributes. TODO: move out of panel state as this couples state to ui.
|
||||
* @property {string} type - Type of the visualization in the panel.
|
||||
* @property {number} panelId - Unique id to represent this panel in the grid.
|
||||
* @property {number} size_x - Width of the panel.
|
||||
* @property {number} size_y - Height of the panel.
|
||||
* @property {number} col - Column index in the grid.
|
||||
* @property {number} row - Row index in the grid.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates and initializes a basic panel state.
|
||||
* @param {number} id
|
||||
* @param {string} type
|
||||
* @param {number} panelId
|
||||
* @return {PanelState}
|
||||
*/
|
||||
export function createPanelState(id, type, panelId) {
|
||||
return {
|
||||
size_x: DEFAULT_PANEL_WIDTH,
|
||||
size_y: DEFAULT_PANEL_HEIGHT,
|
||||
panelId: panelId,
|
||||
type: type,
|
||||
id: id
|
||||
};
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from 'plugins/kibana/dashboard/components/panel/lib/panel_state';
|
||||
|
||||
export class PanelUtils {
|
||||
/**
|
||||
* Fills in default parameters where not specified.
|
||||
* @param {PanelState} panel
|
||||
*/
|
||||
static initializeDefaults(panel) {
|
||||
panel.size_x = panel.size_x || DEFAULT_PANEL_WIDTH;
|
||||
panel.size_y = panel.size_y || DEFAULT_PANEL_HEIGHT;
|
||||
|
||||
if (!panel.id) {
|
||||
// In the interest of backwards comparability
|
||||
if (panel.visId) {
|
||||
panel.id = panel.visId;
|
||||
panel.type = 'visualization';
|
||||
delete panel.visId;
|
||||
} else {
|
||||
throw new Error('Missing object id on panel');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the panel object has the latest size/pos info.
|
||||
* @param {PanelState} panel
|
||||
*/
|
||||
static refreshSizeAndPosition(panel) {
|
||||
const data = panel.$el.coords().grid;
|
||||
panel.size_x = data.size_x;
|
||||
panel.size_y = data.size_y;
|
||||
panel.col = data.col;
|
||||
panel.row = data.row;
|
||||
}
|
||||
|
||||
/**
|
||||
* $el is a circular structure because it contains a reference to it's parent panel,
|
||||
* so it needs to be removed before it can be serialized (we also don't
|
||||
* want it to show up in the url).
|
||||
* @param {PanelState} panel
|
||||
*/
|
||||
static makeSerializeable(panel) {
|
||||
delete panel.$el;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export default function searchLoader(savedSearches, Private) { // Inject services here
|
||||
export function searchLoaderProvider(savedSearches, Private) { // Inject services here
|
||||
return function (panel, $scope) { // Function parameters here
|
||||
return savedSearches.get(panel.id)
|
||||
.then(function (savedSearch) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import UtilsBrushEventProvider from 'ui/utils/brush_event';
|
||||
import FilterBarFilterBarClickHandlerProvider from 'ui/filter_bar/filter_bar_click_handler';
|
||||
|
||||
export default function visualizationLoader(savedVisualizations, Private) { // Inject services here
|
||||
export function visualizationLoaderProvider(savedVisualizations, Private) { // Inject services here
|
||||
const brushEvent = Private(UtilsBrushEventProvider);
|
||||
const filterBarClickHandler = Private(FilterBarFilterBarClickHandlerProvider);
|
||||
|
||||
|
@ -10,7 +10,7 @@ export default function visualizationLoader(savedVisualizations, Private) { // I
|
|||
.then(function (savedVis) {
|
||||
// $scope.state comes via $scope inheritence from the dashboard app. Don't love this.
|
||||
savedVis.vis.listeners.click = filterBarClickHandler($scope.state);
|
||||
savedVis.vis.listeners.brush = brushEvent;
|
||||
savedVis.vis.listeners.brush = brushEvent($scope.state);
|
||||
|
||||
return {
|
||||
savedObj: savedVis,
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
{{::savedObj.title}}
|
||||
</span>
|
||||
<div class="btn-group">
|
||||
<a aria-label="Edit" ng-show="chrome.getVisible() && editUrl" ng-href="{{::editUrl}}">
|
||||
<a aria-label="Edit" ng-show="!isFullScreenMode && editUrl" ng-href="{{::editUrl}}">
|
||||
<i aria-hidden="true" class="fa fa-pencil"></i>
|
||||
</a>
|
||||
<a aria-label="Move" ng-show="chrome.getVisible()" class="panel-move">
|
||||
<a aria-label="Move" ng-show="!isFullScreenMode" class="panel-move">
|
||||
<i aria-hidden="true" class="fa fa-arrows"></i>
|
||||
</a>
|
||||
<a aria-label="Remove" ng-show="chrome.getVisible()" ng-click="remove()">
|
||||
<a aria-label="Remove" ng-show="!isFullScreenMode" ng-click="remove()">
|
||||
<i aria-hidden="true" class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -26,15 +26,18 @@
|
|||
ng-switch-when="visualization"
|
||||
vis="savedObj.vis"
|
||||
search-source="savedObj.searchSource"
|
||||
show-spy-panel="chrome.getVisible()"
|
||||
show-spy-panel="!isFullScreenMode"
|
||||
ui-state="uiState"
|
||||
render-counter
|
||||
class="panel-content">
|
||||
</visualize>
|
||||
|
||||
<doc-table ng-switch-when="search"
|
||||
<doc-table
|
||||
ng-switch-when="search"
|
||||
search-source="savedObj.searchSource"
|
||||
sorting="panel.sort"
|
||||
columns="panel.columns"
|
||||
render-counter
|
||||
class="panel-content"
|
||||
filter="filter">
|
||||
</doc-table>
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
import moment from 'moment';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import 'ui/visualize';
|
||||
import 'ui/doc_table';
|
||||
import PluginsKibanaDashboardComponentsPanelLibLoadPanelProvider from 'plugins/kibana/dashboard/components/panel/lib/load_panel';
|
||||
import FilterManagerProvider from 'ui/filter_manager';
|
||||
import UtilsBrushEventProvider from 'ui/utils/brush_event';
|
||||
import uiModules from 'ui/modules';
|
||||
import panelTemplate from 'plugins/kibana/dashboard/components/panel/panel.html';
|
||||
uiModules
|
||||
.get('app/dashboard')
|
||||
.directive('dashboardPanel', function (savedVisualizations, savedSearches, Notifier, Private, $injector) {
|
||||
const loadPanel = Private(PluginsKibanaDashboardComponentsPanelLibLoadPanelProvider);
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const notify = new Notifier();
|
||||
|
||||
const services = require('plugins/kibana/management/saved_object_registry').all().map(function (serviceObj) {
|
||||
const service = $injector.get(serviceObj.service);
|
||||
return {
|
||||
type: service.type,
|
||||
name: serviceObj.service
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const brushEvent = Private(UtilsBrushEventProvider);
|
||||
|
||||
const getPanelId = function (panel) {
|
||||
return ['P', panel.panelIndex].join('-');
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: panelTemplate,
|
||||
requires: '^dashboardGrid',
|
||||
link: function ($scope, $el) {
|
||||
// using $scope inheritance, panels are available in AppState
|
||||
const $state = $scope.state;
|
||||
|
||||
// receives $scope.panel from the dashboard grid directive, seems like should be isolate?
|
||||
$scope.$watch('id', function () {
|
||||
if (!$scope.panel.id || !$scope.panel.type) return;
|
||||
|
||||
loadPanel($scope.panel, $scope)
|
||||
.then(function (panelConfig) {
|
||||
// These could be done in loadPanel, putting them here to make them more explicit
|
||||
$scope.savedObj = panelConfig.savedObj;
|
||||
$scope.editUrl = panelConfig.editUrl;
|
||||
$scope.$on('$destroy', function () {
|
||||
panelConfig.savedObj.destroy();
|
||||
$scope.parentUiState.removeChild(getPanelId(panelConfig.panel));
|
||||
});
|
||||
|
||||
// create child ui state from the savedObj
|
||||
const uiState = panelConfig.uiState || {};
|
||||
$scope.uiState = $scope.parentUiState.createChild(getPanelId(panelConfig.panel), uiState, true);
|
||||
const panelSavedVis = _.get(panelConfig, 'savedObj.vis'); // Sometimes this will be a search, and undef
|
||||
if (panelSavedVis) {
|
||||
panelSavedVis.setUiState($scope.uiState);
|
||||
}
|
||||
|
||||
$scope.filter = function (field, value, operator) {
|
||||
const index = $scope.savedObj.searchSource.get('index').id;
|
||||
filterManager.add(field, value, operator, index);
|
||||
};
|
||||
})
|
||||
.catch(function (e) {
|
||||
$scope.error = e.message;
|
||||
|
||||
// If the savedObjectType matches the panel type, this means the object itself has been deleted,
|
||||
// so we shouldn't even have an edit link. If they don't match, it means something else is wrong
|
||||
// with the object (but the object still exists), so we link to the object editor instead.
|
||||
const objectItselfDeleted = e.savedObjectType === $scope.panel.type;
|
||||
if (objectItselfDeleted) return;
|
||||
|
||||
const type = $scope.panel.type;
|
||||
const id = $scope.panel.id;
|
||||
const service = _.find(services, { type: type });
|
||||
if (!service) return;
|
||||
|
||||
$scope.editUrl = '#management/kibana/objects/' + service.name + '/' + id + '?notFound=' + e.savedObjectType;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$scope.remove = function () {
|
||||
_.pull($state.panels, $scope.panel);
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
import _ from 'lodash';
|
||||
import 'ui/visualize';
|
||||
import 'ui/doc_table';
|
||||
import { loadPanelProvider } from 'plugins/kibana/dashboard/components/panel/lib/load_panel';
|
||||
import FilterManagerProvider from 'ui/filter_manager';
|
||||
import uiModules from 'ui/modules';
|
||||
import panelTemplate from 'plugins/kibana/dashboard/components/panel/panel.html';
|
||||
|
||||
uiModules
|
||||
.get('app/dashboard')
|
||||
.directive('dashboardPanel', function (savedVisualizations, savedSearches, Notifier, Private, $injector) {
|
||||
const loadPanel = Private(loadPanelProvider);
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
|
||||
const services = require('plugins/kibana/management/saved_object_registry').all().map(function (serviceObj) {
|
||||
const service = $injector.get(serviceObj.service);
|
||||
return {
|
||||
type: service.type,
|
||||
name: serviceObj.service
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a unique id for storing the panel state in the persistent ui.
|
||||
* @param {PanelState} panel
|
||||
* @returns {string}
|
||||
*/
|
||||
const getPersistedStateId = function (panel) {
|
||||
return `P-${panel.panelId}`;
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: panelTemplate,
|
||||
scope: {
|
||||
/**
|
||||
* Whether or not the dashboard this panel is contained on is in 'full screen mode'.
|
||||
* @type {boolean}
|
||||
*/
|
||||
isFullScreenMode: '=',
|
||||
/**
|
||||
* The parent's persisted state is used to create a child persisted state for the
|
||||
* panel.
|
||||
* @type {PersistedState}
|
||||
*/
|
||||
parentUiState: '=',
|
||||
/**
|
||||
* Contains information about this panel.
|
||||
* @type {PanelState}
|
||||
*/
|
||||
panel: '=',
|
||||
/**
|
||||
* Handles removing this panel from the grid.
|
||||
* @type {() => void}
|
||||
*/
|
||||
remove: '&'
|
||||
},
|
||||
link: function ($scope, element) {
|
||||
if (!$scope.panel.id || !$scope.panel.type) return;
|
||||
|
||||
loadPanel($scope.panel, $scope)
|
||||
.then(function (panelConfig) {
|
||||
// These could be done in loadPanel, putting them here to make them more explicit
|
||||
$scope.savedObj = panelConfig.savedObj;
|
||||
$scope.editUrl = panelConfig.editUrl;
|
||||
|
||||
element.on('$destroy', function () {
|
||||
panelConfig.savedObj.destroy();
|
||||
$scope.parentUiState.removeChild(getPersistedStateId(panelConfig.panel));
|
||||
$scope.$destroy();
|
||||
});
|
||||
|
||||
// create child ui state from the savedObj
|
||||
const uiState = panelConfig.uiState || {};
|
||||
$scope.uiState = $scope.parentUiState.createChild(getPersistedStateId(panelConfig.panel), uiState, true);
|
||||
const panelSavedVis = _.get(panelConfig, 'savedObj.vis'); // Sometimes this will be a search, and undef
|
||||
if (panelSavedVis) {
|
||||
panelSavedVis.setUiState($scope.uiState);
|
||||
}
|
||||
|
||||
$scope.filter = function (field, value, operator) {
|
||||
const index = $scope.savedObj.searchSource.get('index').id;
|
||||
filterManager.add(field, value, operator, index);
|
||||
};
|
||||
})
|
||||
.catch(function (e) {
|
||||
$scope.error = e.message;
|
||||
|
||||
// If the savedObjectType matches the panel type, this means the object itself has been deleted,
|
||||
// so we shouldn't even have an edit link. If they don't match, it means something else is wrong
|
||||
// with the object (but the object still exists), so we link to the object editor instead.
|
||||
const objectItselfDeleted = e.savedObjectType === $scope.panel.type;
|
||||
if (objectItselfDeleted) return;
|
||||
|
||||
const type = $scope.panel.type;
|
||||
const id = $scope.panel.id;
|
||||
const service = _.find(services, { type: type });
|
||||
if (!service) return;
|
||||
|
||||
$scope.editUrl = '#management/kibana/objects/' + service.name + '/' + id + '?notFound=' + e.savedObjectType;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -3,6 +3,7 @@ import $ from 'jquery';
|
|||
import Binder from 'ui/binder';
|
||||
import 'gridster';
|
||||
import uiModules from 'ui/modules';
|
||||
import { PanelUtils } from 'plugins/kibana/dashboard/components/panel/lib/panel_utils';
|
||||
|
||||
const app = uiModules.get('app/dashboard');
|
||||
|
||||
|
@ -16,7 +17,6 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
$el = $('<ul>').appendTo($container);
|
||||
|
||||
const $window = $(window);
|
||||
const $body = $(document.body);
|
||||
const binder = new Binder($scope);
|
||||
|
||||
// appState from controller
|
||||
|
@ -34,23 +34,27 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
// debounced layout function is safe to call as much as possible
|
||||
const safeLayout = _.debounce(layout, 200);
|
||||
|
||||
$scope.removePanelFromState = (panelId) => {
|
||||
_.remove($scope.state.panels, function (panel) {
|
||||
return panel.panelId === panelId;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the panel with the given id from the $scope.state.panels array. Does not
|
||||
* remove the ui element from gridster - that is triggered by a watcher that is
|
||||
* triggered on changes made to $scope.state.panels.
|
||||
* @param panelId {number}
|
||||
*/
|
||||
$scope.getPanelByPanelId = (panelId) => {
|
||||
return _.find($scope.state.panels, function (panel) {
|
||||
return panel.panelId === panelId;
|
||||
});
|
||||
};
|
||||
|
||||
function init() {
|
||||
$el.addClass('gridster');
|
||||
|
||||
// See issue https://github.com/elastic/kibana/issues/2138 and the
|
||||
// subsequent fix for why we need to sort here. Short story is that
|
||||
// gridster can fail to render widgets in the correct order, depending
|
||||
// on the specific order of the panels.
|
||||
// See https://github.com/ducksboard/gridster.js/issues/147
|
||||
// for some additional back story.
|
||||
$state.panels.sort((a, b) => {
|
||||
if (a.row === b.row) {
|
||||
return a.col - b.col;
|
||||
} else {
|
||||
return a.row - b.row;
|
||||
}
|
||||
});
|
||||
|
||||
gridster = $el.gridster({
|
||||
max_cols: COLS,
|
||||
min_cols: COLS,
|
||||
|
@ -86,10 +90,26 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
const added = _.difference(panels, currentPanels);
|
||||
|
||||
if (removed.length) removed.forEach(removePanel);
|
||||
if (added.length) added.forEach(addPanel);
|
||||
if (added.length) {
|
||||
// See issue https://github.com/elastic/kibana/issues/2138 and the
|
||||
// subsequent fix for why we need to sort here. Short story is that
|
||||
// gridster can fail to render widgets in the correct order, depending
|
||||
// on the specific order of the panels.
|
||||
// See https://github.com/ducksboard/gridster.js/issues/147
|
||||
// for some additional back story.
|
||||
added.sort((a, b) => {
|
||||
if (a.row === b.row) {
|
||||
return a.col - b.col;
|
||||
} else {
|
||||
return a.row - b.row;
|
||||
}
|
||||
});
|
||||
|
||||
added.forEach(addPanel);
|
||||
};
|
||||
|
||||
// ensure that every panel can be serialized now that we are done
|
||||
$state.panels.forEach(makePanelSerializeable);
|
||||
$state.panels.forEach(PanelUtils.makeSerializeable);
|
||||
|
||||
// alert interested parties that we have finished processing changes to the panels
|
||||
// TODO: change this from event based to calling a method on dashboardApp
|
||||
|
@ -107,7 +127,7 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
panel.$el.stop();
|
||||
removePanel(panel, true);
|
||||
// not that we will, but lets be safe
|
||||
makePanelSerializeable(panel);
|
||||
PanelUtils.makeSerializeable(panel);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -120,81 +140,44 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
// return the panel object for an element.
|
||||
//
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// ALWAYS CALL makePanelSerializeable AFTER YOU ARE DONE WITH IT
|
||||
// ALWAYS CALL PanelUtils.makeSerializeable AFTER YOU ARE DONE WITH IT
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
function getPanelFor(el) {
|
||||
const $panel = el.jquery ? el : $(el);
|
||||
const panel = $panel.data('panel');
|
||||
|
||||
panel.$el = $panel;
|
||||
panel.$scope = $panel.data('$scope');
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
// since the $el and $scope are circular structures, they need to be
|
||||
// removed from panel before it can be serialized (we also wouldn't
|
||||
// want them to show up in the url)
|
||||
function makePanelSerializeable(panel) {
|
||||
delete panel.$el;
|
||||
delete panel.$scope;
|
||||
}
|
||||
|
||||
// tell gridster to remove the panel, and cleanup our metadata
|
||||
function removePanel(panel, silent) {
|
||||
// remove from grister 'silently' (don't reorganize after)
|
||||
gridster.remove_widget(panel.$el, silent);
|
||||
|
||||
// destroy the scope
|
||||
panel.$scope.$destroy();
|
||||
|
||||
panel.$el.removeData('panel');
|
||||
panel.$el.removeData('$scope');
|
||||
}
|
||||
|
||||
// tell gridster to add the panel, and create additional meatadata like $scope
|
||||
function addPanel(panel) {
|
||||
_.defaults(panel, {
|
||||
size_x: 3,
|
||||
size_y: 2
|
||||
});
|
||||
PanelUtils.initializeDefaults(panel);
|
||||
|
||||
// ignore panels that don't have vis id's
|
||||
if (!panel.id) {
|
||||
// In the interest of backwards compat
|
||||
if (panel.visId) {
|
||||
panel.id = panel.visId;
|
||||
panel.type = 'visualization';
|
||||
delete panel.visId;
|
||||
} else {
|
||||
throw new Error('missing object id on panel');
|
||||
}
|
||||
}
|
||||
|
||||
panel.$scope = $scope.$new();
|
||||
panel.$scope.panel = panel;
|
||||
panel.$scope.parentUiState = $scope.uiState;
|
||||
|
||||
panel.$el = $compile('<li><dashboard-panel></li>')(panel.$scope);
|
||||
const panelHtml = `
|
||||
<li>
|
||||
<dashboard-panel remove="removePanelFromState(${panel.panelId})"
|
||||
panel="getPanelByPanelId(${panel.panelId})"
|
||||
is-full-screen-mode="!chrome.getVisible()"
|
||||
parent-ui-state="uiState">
|
||||
</li>`;
|
||||
panel.$el = $compile(panelHtml)($scope);
|
||||
|
||||
// tell gridster to use the widget
|
||||
gridster.add_widget(panel.$el, panel.size_x, panel.size_y, panel.col, panel.row);
|
||||
|
||||
// update size/col/etc.
|
||||
refreshPanelStats(panel);
|
||||
// Gridster may change the position of the widget when adding it, make sure the panel
|
||||
// contains the latest info.
|
||||
PanelUtils.refreshSizeAndPosition(panel);
|
||||
|
||||
// stash the panel and it's scope in the element's data
|
||||
// stash the panel in the element's data
|
||||
panel.$el.data('panel', panel);
|
||||
panel.$el.data('$scope', panel.$scope);
|
||||
}
|
||||
|
||||
// ensure that the panel object has the latest size/pos info
|
||||
function refreshPanelStats(panel) {
|
||||
const data = panel.$el.coords().grid;
|
||||
panel.size_x = data.size_x;
|
||||
panel.size_y = data.size_y;
|
||||
panel.col = data.col;
|
||||
panel.row = data.row;
|
||||
}
|
||||
|
||||
// when gridster tell us it made a change, update each of the panel objects
|
||||
|
@ -202,9 +185,8 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
// ensure that our panel objects keep their size in sync
|
||||
gridster.$widgets.each(function (i, el) {
|
||||
const panel = getPanelFor(el);
|
||||
refreshPanelStats(panel);
|
||||
panel.$scope.$broadcast('resize');
|
||||
makePanelSerializeable(panel);
|
||||
PanelUtils.refreshSizeAndPosition(panel);
|
||||
PanelUtils.makeSerializeable(panel);
|
||||
$scope.$root.$broadcast('change:vis');
|
||||
});
|
||||
}
|
||||
|
@ -221,7 +203,6 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
g.min_widget_width = (g.options.widget_margins[0] * 2) + g.options.widget_base_dimensions[0];
|
||||
g.min_widget_height = (g.options.widget_margins[1] * 2) + g.options.widget_base_dimensions[1];
|
||||
|
||||
// const serializedGrid = g.serialize();
|
||||
g.$widgets.each(function (i, widget) {
|
||||
g.resize_widget($(widget));
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
>
|
||||
<span
|
||||
ng-show="dash.id"
|
||||
ng-bind="::dash.title"
|
||||
ng-bind="dash.lastSavedTitle"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import angular from 'angular';
|
||||
import chrome from 'ui/chrome';
|
||||
import 'ui/courier';
|
||||
|
@ -8,7 +7,7 @@ import 'ui/notify';
|
|||
import 'ui/typeahead';
|
||||
import 'ui/share';
|
||||
import 'plugins/kibana/dashboard/directives/grid';
|
||||
import 'plugins/kibana/dashboard/components/panel/panel';
|
||||
import 'plugins/kibana/dashboard/directives/dashboard_panel';
|
||||
import 'plugins/kibana/dashboard/services/saved_dashboards';
|
||||
import 'plugins/kibana/dashboard/styles/main.less';
|
||||
import FilterBarQueryFilterProvider from 'ui/filter_bar/query_filter';
|
||||
|
@ -17,8 +16,9 @@ import stateMonitorFactory from 'ui/state_management/state_monitor_factory';
|
|||
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'));
|
||||
import { savedDashboardRegister } from 'plugins/kibana/dashboard/services/saved_dashboard_register';
|
||||
import { createPanelState } from 'plugins/kibana/dashboard/components/panel/lib/panel_state';
|
||||
require('ui/saved_objects/saved_object_registry').register(savedDashboardRegister);
|
||||
|
||||
const app = uiModules.get('app/dashboard', [
|
||||
'elasticsearch',
|
||||
|
@ -108,26 +108,32 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
key: 'new',
|
||||
description: 'New Dashboard',
|
||||
run: function () { kbnUrl.change('/dashboard', {}); },
|
||||
testId: 'dashboardNewButton',
|
||||
}, {
|
||||
key: 'add',
|
||||
description: 'Add a panel to the dashboard',
|
||||
template: require('plugins/kibana/dashboard/partials/pick_visualization.html')
|
||||
template: require('plugins/kibana/dashboard/partials/pick_visualization.html'),
|
||||
testId: 'dashboardAddPanelButton',
|
||||
}, {
|
||||
key: 'save',
|
||||
description: 'Save Dashboard',
|
||||
template: require('plugins/kibana/dashboard/partials/save_dashboard.html')
|
||||
template: require('plugins/kibana/dashboard/partials/save_dashboard.html'),
|
||||
testId: 'dashboardSaveButton',
|
||||
}, {
|
||||
key: 'open',
|
||||
description: 'Open Saved Dashboard',
|
||||
template: require('plugins/kibana/dashboard/partials/load_dashboard.html')
|
||||
template: require('plugins/kibana/dashboard/partials/load_dashboard.html'),
|
||||
testId: 'dashboardOpenButton',
|
||||
}, {
|
||||
key: 'share',
|
||||
description: 'Share Dashboard',
|
||||
template: require('plugins/kibana/dashboard/partials/share.html')
|
||||
template: require('plugins/kibana/dashboard/partials/share.html'),
|
||||
testId: 'dashboardShareButton',
|
||||
}, {
|
||||
key: 'options',
|
||||
description: 'Options',
|
||||
template: require('plugins/kibana/dashboard/partials/options.html')
|
||||
template: require('plugins/kibana/dashboard/partials/options.html'),
|
||||
testId: 'dashboardOptionsButton',
|
||||
}];
|
||||
|
||||
$scope.refresh = _.bindKey(courier, 'fetch');
|
||||
|
@ -138,15 +144,16 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
|
||||
courier.setRootSearchSource(dash.searchSource);
|
||||
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
|
||||
function init() {
|
||||
updateQueryOnRootSource();
|
||||
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
if (dash.id) {
|
||||
docTitle.change(dash.title);
|
||||
}
|
||||
|
||||
initPanelIndices();
|
||||
initPanelIds();
|
||||
|
||||
// watch for state changes and update the appStatus.dirty value
|
||||
stateMonitor = stateMonitorFactory.create($state, stateDefaults);
|
||||
|
@ -165,24 +172,23 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
$scope.$emit('application.load');
|
||||
}
|
||||
|
||||
function initPanelIndices() {
|
||||
// find the largest panelIndex in all the panels
|
||||
let maxIndex = getMaxPanelIndex();
|
||||
function initPanelIds() {
|
||||
// find the largest panelId in all the panels
|
||||
let maxIndex = getMaxPanelId();
|
||||
|
||||
// ensure that all panels have a panelIndex
|
||||
// ensure that all panels have a panelId
|
||||
$scope.state.panels.forEach(function (panel) {
|
||||
if (!panel.panelIndex) {
|
||||
panel.panelIndex = maxIndex++;
|
||||
if (!panel.panelId) {
|
||||
panel.panelId = maxIndex++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getMaxPanelIndex() {
|
||||
let index = $scope.state.panels.reduce(function (idx, panel) {
|
||||
// if panel is missing an index, add one and increment the index
|
||||
return Math.max(idx, panel.panelIndex || idx);
|
||||
function getMaxPanelId() {
|
||||
let maxId = $scope.state.panels.reduce(function (id, panel) {
|
||||
return Math.max(id, panel.panelId || id);
|
||||
}, 0);
|
||||
return ++index;
|
||||
return ++maxId;
|
||||
}
|
||||
|
||||
function updateQueryOnRootSource() {
|
||||
|
@ -222,7 +228,6 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
};
|
||||
|
||||
$scope.save = function () {
|
||||
$state.title = dash.id = dash.title;
|
||||
$state.save();
|
||||
|
||||
const timeRestoreObj = _.pick(timefilter.refreshInterval, ['display', 'pause', 'section', 'value']);
|
||||
|
@ -241,6 +246,8 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
notify.info('Saved Dashboard as "' + dash.title + '"');
|
||||
if (dash.id !== $routeParams.id) {
|
||||
kbnUrl.change('/dashboard/{{id}}', {id: dash.id});
|
||||
} else {
|
||||
docTitle.change(dash.lastSavedTitle);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -265,12 +272,12 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
// called by the saved-object-finder when a user clicks a vis
|
||||
$scope.addVis = function (hit) {
|
||||
pendingVis++;
|
||||
$state.panels.push({ id: hit.id, type: 'visualization', panelIndex: getMaxPanelIndex() });
|
||||
$state.panels.push(createPanelState(hit.id, 'visualization', getMaxPanelId()));
|
||||
};
|
||||
|
||||
$scope.addSearch = function (hit) {
|
||||
pendingVis++;
|
||||
$state.panels.push({ id: hit.id, type: 'search', panelIndex: getMaxPanelIndex() });
|
||||
$state.panels.push(createPanelState(hit.id, 'search', getMaxPanelId()));
|
||||
};
|
||||
|
||||
// Setup configurable values for config directive, after objects are initialized
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
role="form"
|
||||
ng-submit="opts.save()"
|
||||
>
|
||||
<div class="localDropdownTitle">Save Dashboard</div>
|
||||
<div class="localDropdownTitle">Save {{opts.dashboard.getDisplayName()}}</div>
|
||||
<input
|
||||
class="localDropdownInput"
|
||||
id="dashboardTitle"
|
||||
|
@ -11,12 +11,18 @@
|
|||
placeholder="Dashboard title"
|
||||
input-focus="select"
|
||||
>
|
||||
|
||||
<saved-object-save-as-check-box saved-object="opts.dashboard"></saved-object-save-as-check-box>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="opts.dashboard.timeRestore" ng-checked="opts.dashboard.timeRestore">
|
||||
Store time with dashboard
|
||||
<kbn-info info="Change the time filter to the currently selected time each time this dashboard is loaded"></kbn-info>
|
||||
Store time with {{opts.dashboard.getDisplayName()}}
|
||||
<kbn-info placement="bottom" info="Change the time filter to the currently selected time each time this dashboard is loaded"></kbn-info>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" ng-disabled="!opts.dashboard.title" class="btn btn-primary" aria-label="Save dashboard">Save</button>
|
||||
|
||||
<button type="submit" ng-disabled="!opts.dashboard.title" class="btn btn-primary" aria-label="Save dashboard">
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import uiModules from 'ui/modules';
|
||||
const module = uiModules.get('app/dashboard');
|
||||
|
||||
|
@ -47,20 +46,20 @@ module.factory('SavedDashboard', function (courier, config) {
|
|||
|
||||
// if type:dashboard has no mapping, we push this mapping into ES
|
||||
SavedDashboard.mapping = {
|
||||
title: 'string',
|
||||
title: 'text',
|
||||
hits: 'integer',
|
||||
description: 'string',
|
||||
panelsJSON: 'string',
|
||||
optionsJSON: 'string',
|
||||
uiStateJSON: 'string',
|
||||
description: 'text',
|
||||
panelsJSON: 'text',
|
||||
optionsJSON: 'text',
|
||||
uiStateJSON: 'text',
|
||||
version: 'integer',
|
||||
timeRestore: 'boolean',
|
||||
timeTo: 'string',
|
||||
timeFrom: 'string',
|
||||
timeTo: 'keyword',
|
||||
timeFrom: 'keyword',
|
||||
refreshInterval: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
display: {type: 'string'},
|
||||
display: {type: 'keyword'},
|
||||
pause: { type: 'boolean'},
|
||||
section: { type: 'integer'},
|
||||
value: { type: 'integer'}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export default function savedDashboardFn(savedDashboards) {
|
||||
export function savedDashboardRegister(savedDashboards) {
|
||||
return savedDashboards;
|
||||
};
|
||||
|
|
|
@ -115,19 +115,23 @@ function discoverController($scope, config, courier, $route, $window, Notifier,
|
|||
$scope.topNavMenu = [{
|
||||
key: 'new',
|
||||
description: 'New Search',
|
||||
run: function () { kbnUrl.change('/discover'); }
|
||||
run: function () { kbnUrl.change('/discover'); },
|
||||
testId: 'discoverNewButton',
|
||||
}, {
|
||||
key: 'save',
|
||||
description: 'Save Search',
|
||||
template: require('plugins/kibana/discover/partials/save_search.html')
|
||||
template: require('plugins/kibana/discover/partials/save_search.html'),
|
||||
testId: 'discoverSaveButton',
|
||||
}, {
|
||||
key: 'open',
|
||||
description: 'Load Saved Search',
|
||||
template: require('plugins/kibana/discover/partials/load_search.html')
|
||||
description: 'Open Saved Search',
|
||||
template: require('plugins/kibana/discover/partials/load_search.html'),
|
||||
testId: 'discoverOpenButton',
|
||||
}, {
|
||||
key: 'share',
|
||||
description: 'Share Search',
|
||||
template: require('plugins/kibana/discover/partials/share_search.html')
|
||||
template: require('plugins/kibana/discover/partials/share_search.html'),
|
||||
testId: 'discoverShareButton',
|
||||
}];
|
||||
$scope.timefilter = timefilter;
|
||||
|
||||
|
@ -314,7 +318,6 @@ function discoverController($scope, config, courier, $route, $window, Notifier,
|
|||
$scope.opts.saveDataSource = function () {
|
||||
return $scope.updateDataSource()
|
||||
.then(function () {
|
||||
savedSearch.id = savedSearch.title;
|
||||
savedSearch.columns = $scope.state.columns;
|
||||
savedSearch.sort = $scope.state.sort;
|
||||
|
||||
|
@ -330,6 +333,7 @@ function discoverController($scope, config, courier, $route, $window, Notifier,
|
|||
} else {
|
||||
// Update defaults so that "reload saved query" functions correctly
|
||||
$state.setDefaults(getStateDefaults());
|
||||
docTitle.change(savedSearch.lastSavedTitle);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -543,7 +547,7 @@ function discoverController($scope, config, courier, $route, $window, Notifier,
|
|||
timefilter.time.to = moment(e.point.x + e.data.ordered.interval);
|
||||
timefilter.time.mode = 'absolute';
|
||||
},
|
||||
brush: brushEvent
|
||||
brush: brushEvent($scope.state)
|
||||
},
|
||||
aggs: visStateAggs
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<div data-transclude-slot="topLeftCorner" class="localBreadcrumbs">
|
||||
<div class="localBreadcrumb">
|
||||
<span ng-show="opts.savedSearch.id" class="localBreadcrumb__emphasis">
|
||||
<span data-test-subj="discoverCurrentQuery" ng-bind="::opts.savedSearch.title"></span>
|
||||
<span data-test-subj="discoverCurrentQuery" ng-bind="opts.savedSearch.lastSavedTitle"></span>
|
||||
<i aria-label="Reload Saved Search" tooltip="Reload Saved Search" ng-click="resetQuery();" class="fa fa-undo small"></i>
|
||||
</span>
|
||||
<span data-test-subj="discoverQueryHits" class="localBreadcrumb__emphasis">{{(hits || 0) | number:0}}</span>
|
||||
|
@ -125,7 +125,8 @@
|
|||
sorting="state.sort"
|
||||
columns="state.columns"
|
||||
infinite-scroll="true"
|
||||
filter="filterQuery">
|
||||
filter="filterQuery"
|
||||
render-counter>
|
||||
</doc-table>
|
||||
|
||||
<div ng-if="rows.length == opts.sampleSize" class="discover-table-footer">
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
input-focus="select"
|
||||
placeholder="Name this search..."
|
||||
>
|
||||
|
||||
<saved-object-save-as-check-box saved-object="opts.savedSearch"></saved-object-save-as-check-box>
|
||||
<button ng-disabled="!opts.savedSearch.title" data-test-subj="discover-save-search-btn" type="submit" class="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
|
|
|
@ -31,11 +31,11 @@ module.factory('SavedSearch', function (courier) {
|
|||
SavedSearch.type = 'search';
|
||||
|
||||
SavedSearch.mapping = {
|
||||
title: 'string',
|
||||
description: 'string',
|
||||
title: 'text',
|
||||
description: 'text',
|
||||
hits: 'integer',
|
||||
columns: 'string',
|
||||
sort: 'string',
|
||||
columns: 'keyword',
|
||||
sort: 'keyword',
|
||||
version: 'integer'
|
||||
};
|
||||
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
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;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,94 +0,0 @@
|
|||
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;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
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
|
||||
};
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
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];
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
import './directives/pipeline_setup';
|
|
@ -1,74 +0,0 @@
|
|||
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);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,86 +0,0 @@
|
|||
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);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,480 +0,0 @@
|
|||
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 () { });
|
||||
|
||||
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
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();
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
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;
|
||||
};
|
|
@ -1,176 +0,0 @@
|
|||
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();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
<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>
|
|
@ -1,23 +0,0 @@
|
|||
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 || []
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
<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>
|
|
@ -1,28 +0,0 @@
|
|||
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'
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,58 +0,0 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
processor-ui-date {
|
||||
.custom-date-format {
|
||||
display: flex;
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
<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>
|
|
@ -1,32 +0,0 @@
|
|||
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 || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,61 +0,0 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
processor-ui-geoip {
|
||||
.advanced-section {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.advanced-section-heading {
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
border: transparent;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
<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>
|
|
@ -1,28 +0,0 @@
|
|||
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 || []
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,16 +0,0 @@
|
|||
<div class="form-group">
|
||||
<label>Field:</label>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-options="field as field for field in fields"
|
||||
ng-model="processor.sourceField">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Field Data:</label>
|
||||
<pre>{{ fieldData }}</pre>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Pattern:</label>
|
||||
<input type="text" class="form-control" ng-model="processor.pattern">
|
||||
</div>
|
|
@ -1,30 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import keysDeep from '../../lib/keys_deep';
|
||||
import Processor from '../base/view_model';
|
||||
|
||||
export class Grok extends Processor {
|
||||
constructor(processorId) {
|
||||
super(processorId, 'grok', 'Grok');
|
||||
this.sourceField = '';
|
||||
this.pattern = '';
|
||||
}
|
||||
|
||||
get description() {
|
||||
const inputKeys = keysDeep(this.inputObject);
|
||||
const outputKeys = keysDeep(this.outputObject);
|
||||
const addedKeys = _.difference(outputKeys, inputKeys);
|
||||
const added = addedKeys.sort().map(field => `[${field}]`).join(', ');
|
||||
const source = this.sourceField || '?';
|
||||
|
||||
return `[${source}] -> ${added}`;
|
||||
}
|
||||
|
||||
get model() {
|
||||
return {
|
||||
processorId: this.processorId,
|
||||
typeId: this.typeId,
|
||||
sourceField: this.sourceField || '',
|
||||
pattern: this.pattern || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import uiModules from 'ui/modules';
|
||||
import keysDeep from '../../lib/keys_deep';
|
||||
import template from './view.html';
|
||||
|
||||
const app = uiModules.get('kibana');
|
||||
|
||||
//scope.processor, scope.pipeline are attached by the process_container.
|
||||
app.directive('processorUiGsub', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller : function ($scope) {
|
||||
const processor = $scope.processor;
|
||||
const pipeline = $scope.pipeline;
|
||||
|
||||
function consumeNewInputObject() {
|
||||
$scope.fields = keysDeep(processor.inputObject);
|
||||
refreshFieldData();
|
||||
}
|
||||
|
||||
function refreshFieldData() {
|
||||
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
|
||||
}
|
||||
|
||||
function processorUiChanged() {
|
||||
pipeline.setDirty();
|
||||
}
|
||||
|
||||
$scope.$watch('processor.inputObject', consumeNewInputObject);
|
||||
|
||||
$scope.$watch('processor.sourceField', () => {
|
||||
refreshFieldData();
|
||||
processorUiChanged();
|
||||
});
|
||||
|
||||
$scope.$watch('processor.pattern', processorUiChanged);
|
||||
$scope.$watch('processor.replacement', processorUiChanged);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
<div class="form-group">
|
||||
<label>Field:</label>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-options="field as field for field in fields"
|
||||
ng-model="processor.sourceField">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Field Data:</label>
|
||||
<pre>{{ fieldData }}</pre>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Pattern:</label>
|
||||
<input type="text" class="form-control" ng-model="processor.pattern">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Replacement:</label>
|
||||
<input type="text" class="form-control" ng-trim="false" ng-model="processor.replacement">
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
import Processor from '../base/view_model';
|
||||
|
||||
export class Gsub extends Processor {
|
||||
constructor(processorId) {
|
||||
super(processorId, 'gsub', 'Gsub');
|
||||
this.sourceField = '';
|
||||
this.pattern = '';
|
||||
this.replacement = '';
|
||||
}
|
||||
|
||||
get description() {
|
||||
const source = this.sourceField || '?';
|
||||
return `[${source}] - /${this.pattern}/ -> '${this.replacement}'`;
|
||||
}
|
||||
|
||||
get model() {
|
||||
return {
|
||||
processorId: this.processorId,
|
||||
typeId: this.typeId,
|
||||
sourceField: this.sourceField || '',
|
||||
pattern: this.pattern || '',
|
||||
replacement: this.replacement || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import './append/directive';
|
||||
import './convert/directive';
|
||||
import './date/directive';
|
||||
import './geoip/directive';
|
||||
import './grok/directive';
|
||||
import './gsub/directive';
|
||||
import './join/directive';
|
||||
import './lowercase/directive';
|
||||
import './remove/directive';
|
||||
import './rename/directive';
|
||||
import './set/directive';
|
||||
import './split/directive';
|
||||
import './trim/directive';
|
||||
import './uppercase/directive';
|
|
@ -1,41 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import uiModules from 'ui/modules';
|
||||
import keysDeep from '../../lib/keys_deep';
|
||||
import template from './view.html';
|
||||
|
||||
const app = uiModules.get('kibana');
|
||||
|
||||
//scope.processor, scope.pipeline are attached by the process_container.
|
||||
app.directive('processorUiJoin', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller : function ($scope) {
|
||||
const processor = $scope.processor;
|
||||
const pipeline = $scope.pipeline;
|
||||
|
||||
function consumeNewInputObject() {
|
||||
const allKeys = keysDeep(processor.inputObject);
|
||||
$scope.fields = _.filter(allKeys, (key) => { return _.isArray(_.get(processor.inputObject, key)); });
|
||||
refreshFieldData();
|
||||
}
|
||||
|
||||
function refreshFieldData() {
|
||||
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
|
||||
}
|
||||
|
||||
function processorUiChanged() {
|
||||
pipeline.setDirty();
|
||||
}
|
||||
|
||||
$scope.$watch('processor.inputObject', consumeNewInputObject);
|
||||
|
||||
$scope.$watch('processor.sourceField', () => {
|
||||
refreshFieldData();
|
||||
processorUiChanged();
|
||||
});
|
||||
|
||||
$scope.$watch('processor.separator', processorUiChanged);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,16 +0,0 @@
|
|||
<div class="form-group">
|
||||
<label>Array Field:</label>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-options="field as field for field in fields"
|
||||
ng-model="processor.sourceField">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Field Data:</label>
|
||||
<pre>{{ fieldData }}</pre>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Separator:</label>
|
||||
<input type="text" class="form-control" ng-trim="false" ng-model="processor.separator">
|
||||
</div>
|
|
@ -1,24 +0,0 @@
|
|||
import Processor from '../base/view_model';
|
||||
|
||||
export class Join extends Processor {
|
||||
constructor(processorId) {
|
||||
super(processorId, 'join', 'Join');
|
||||
this.sourceField = '';
|
||||
this.separator = '';
|
||||
}
|
||||
|
||||
get description() {
|
||||
const source = this.sourceField || '?';
|
||||
const separator = this.separator ? ` on '${this.separator}'` : '';
|
||||
return `[${source}]${separator}`;
|
||||
}
|
||||
|
||||
get model() {
|
||||
return {
|
||||
processorId: this.processorId,
|
||||
typeId: this.typeId,
|
||||
sourceField: this.sourceField || '',
|
||||
separator: this.separator || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import uiModules from 'ui/modules';
|
||||
import keysDeep from '../../lib/keys_deep';
|
||||
import template from './view.html';
|
||||
|
||||
const app = uiModules.get('kibana');
|
||||
|
||||
//scope.processor, scope.pipeline are attached by the process_container.
|
||||
app.directive('processorUiLowercase', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller : function ($scope) {
|
||||
const processor = $scope.processor;
|
||||
const pipeline = $scope.pipeline;
|
||||
|
||||
function consumeNewInputObject() {
|
||||
const allKeys = keysDeep(processor.inputObject);
|
||||
$scope.fields = _.filter(allKeys, (key) => { return _.isString(_.get(processor.inputObject, key)); });
|
||||
refreshFieldData();
|
||||
}
|
||||
|
||||
function refreshFieldData() {
|
||||
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
|
||||
}
|
||||
|
||||
function processorUiChanged() {
|
||||
pipeline.setDirty();
|
||||
}
|
||||
|
||||
$scope.$watch('processor.inputObject', consumeNewInputObject);
|
||||
|
||||
$scope.$watch('processor.sourceField', () => {
|
||||
refreshFieldData();
|
||||
processorUiChanged();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
<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>
|
|
@ -1,21 +0,0 @@
|
|||
import Processor from '../base/view_model';
|
||||
|
||||
export class Lowercase extends Processor {
|
||||
constructor(processorId) {
|
||||
super(processorId, 'lowercase', 'Lowercase');
|
||||
this.sourceField = '';
|
||||
}
|
||||
|
||||
get description() {
|
||||
const source = this.sourceField || '?';
|
||||
return `[${source}]`;
|
||||
}
|
||||
|
||||
get model() {
|
||||
return {
|
||||
processorId: this.processorId,
|
||||
typeId: this.typeId,
|
||||
sourceField: this.sourceField || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,38 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import uiModules from 'ui/modules';
|
||||
import keysDeep from '../../lib/keys_deep';
|
||||
import template from './view.html';
|
||||
|
||||
const app = uiModules.get('kibana');
|
||||
|
||||
//scope.processor, scope.pipeline are attached by the process_container.
|
||||
app.directive('processorUiRemove', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller : function ($scope) {
|
||||
const processor = $scope.processor;
|
||||
const pipeline = $scope.pipeline;
|
||||
|
||||
function consumeNewInputObject() {
|
||||
$scope.fields = keysDeep(processor.inputObject);
|
||||
refreshFieldData();
|
||||
}
|
||||
|
||||
function refreshFieldData() {
|
||||
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
|
||||
}
|
||||
|
||||
function processorUiChanged() {
|
||||
pipeline.setDirty();
|
||||
}
|
||||
|
||||
$scope.$watch('processor.inputObject', consumeNewInputObject);
|
||||
|
||||
$scope.$watch('processor.sourceField', () => {
|
||||
refreshFieldData();
|
||||
processorUiChanged();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
<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>
|
|
@ -1,21 +0,0 @@
|
|||
import Processor from '../base/view_model';
|
||||
|
||||
export class Remove extends Processor {
|
||||
constructor(processorId) {
|
||||
super(processorId, 'remove', 'Remove');
|
||||
this.sourceField = '';
|
||||
}
|
||||
|
||||
get description() {
|
||||
const source = this.sourceField || '?';
|
||||
return `[${source}]`;
|
||||
}
|
||||
|
||||
get model() {
|
||||
return {
|
||||
processorId: this.processorId,
|
||||
typeId: this.typeId,
|
||||
sourceField: this.sourceField || ''
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import uiModules from 'ui/modules';
|
||||
import keysDeep from '../../lib/keys_deep';
|
||||
import template from './view.html';
|
||||
|
||||
const app = uiModules.get('kibana');
|
||||
|
||||
//scope.processor, scope.pipeline are attached by the process_container.
|
||||
app.directive('processorUiRename', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller : function ($scope) {
|
||||
const processor = $scope.processor;
|
||||
const pipeline = $scope.pipeline;
|
||||
|
||||
function consumeNewInputObject() {
|
||||
$scope.fields = keysDeep(processor.inputObject);
|
||||
refreshFieldData();
|
||||
}
|
||||
|
||||
function refreshFieldData() {
|
||||
$scope.fieldData = _.get(processor.inputObject, processor.sourceField);
|
||||
}
|
||||
|
||||
function processorUiChanged() {
|
||||
pipeline.setDirty();
|
||||
}
|
||||
|
||||
$scope.$watch('processor.inputObject', consumeNewInputObject);
|
||||
|
||||
$scope.$watch('processor.sourceField', () => {
|
||||
refreshFieldData();
|
||||
processorUiChanged();
|
||||
});
|
||||
|
||||
$scope.$watch('processor.targetField', processorUiChanged);
|
||||
}
|
||||
};
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue