Merge branch 'master' into issues/1726

This commit is contained in:
Jonathan Budzenski 2015-09-24 09:12:10 -05:00
commit d58d14d7fd
192 changed files with 4450 additions and 1632 deletions

View file

@ -10,4 +10,5 @@ trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
insert_final_newline = false

View file

@ -1,6 +1,9 @@
---
parser: babel-eslint
plugins:
- mocha
env:
es6: true
amd: true
@ -67,3 +70,6 @@ rules:
valid-typeof: 2
wrap-iife: [ 2, outside ]
yoda: 0
mocha/no-exclusive-tests: 2
mocha/handle-done-callback: 2

View file

@ -1,10 +1,11 @@
language: node_js
node_js: '0.12.7'
install:
- npm install -g npm@3.2
- npm install
script: ./node_modules/.bin/grunt travis
sudo: false
addons:
firefox: "40.0"
cache:
directories:
- esvm

View file

@ -23,12 +23,6 @@ Please make sure you have signed the [Contributor License Agreement](http://www.
nvm install "$(cat .node-version)"
```
- Install npm 3.2
```sh
npm install -g npm@3.2
```
- Install dependencies
```sh
@ -44,7 +38,7 @@ Please make sure you have signed the [Contributor License Agreement](http://www.
- Start the development server.
```sh
./bin/kibana --dev
npm start
```
#### `config/kibana.dev.yml`
@ -73,6 +67,7 @@ Here are some hints for getting eslint setup in your favorite editor:
| IntelliJ | Settings » Languages & Frameworks » JavaScript » Code Quality Tools » ESLint |
| vi | [scrooloose/syntastic](https://github.com/scrooloose/syntastic) |
Another tool we use for enforcing consistent coding style is Editorconfig, which can be set up by installing a plugin in your editor that dynamically updates its configuration. Take a look at the [Editorconfig](http://editorconfig.org/#download) site to find a plugin for your editor, and browse our [`.editorconfig`](https://github.com/elastic/kibana/blob/master/.editorconfig) file to see what config rules we set up.
### Testing and building

View file

@ -45,7 +45,22 @@ module.exports = function (grunt) {
'<%= root %>/tasks/**/*.js',
'<%= src %>/**/*.js',
'!<%= src %>/fixtures/**/*.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'
}
};
grunt.config.merge(config);

View file

@ -1,4 +1,4 @@
# Kibana 4.2.0-snapshot
# Kibana 4.3.0-snapshot
[![Build Status](https://travis-ci.org/elastic/kibana.svg?branch=master)](https://travis-ci.org/elastic/kibana?branch=master)

View file

@ -25,7 +25,11 @@
# elasticsearch.username: user
# elasticsearch.password: pass
# If your Elasticsearch requires client certificate and key
# SSL for outgoing requests from the Kibana Server to the browser (PEM formatted)
# server.ssl.cert: /path/to/your/server.crt
# server.ssl.key: /path/to/your/server.key
# Optional setting to validate that your Elasticsearch backend uses the same key files (PEM formatted)
# elasticsearch.ssl.cert: /path/to/your/client.crt
# elasticsearch.ssl.key: /path/to/your/client.key
@ -52,12 +56,17 @@
# Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying
# elasticsearch.startupTimeout: 5000
# SSL for outgoing requests from the Kibana Server (PEM formatted)
# server.ssl.cert: /path/to/your/server.crt
# server.ssl.key: /path/to/your/server.key
# Set the path to where you would like the process id file to be created.
# pid.file: /var/run/kibana.pid
# If you would like to send the log output to a file you can set the path below.
# logging.dest: stdout
# Set this to true to suppress all logging output.
# logging.silent: false
# Set this to true to suppress all logging output except for error messages.
# logging.quiet: false
# Set this to true to log all events, including system usage information and all requests.
# logging.verbose

View file

@ -19,9 +19,14 @@ You need at least one saved <<visualize, visualization>> to use a dashboard.
The first time you click the *Dashboard* tab, Kibana displays an empty dashboard.
image:images/NewDashboard.jpg[New Dashboard screen]
image:images/NewDashboard.png[New Dashboard screen]
Build your dashboard by adding visualizations.
Build your dashboard by adding visualizations. By default, Kibana dashboards use a light color theme. To use a dark color
theme instead, click the *Settings* image:images/SettingsButton.jpg[Gear] button and check the *Use dark theme* box.
image:images/darktheme.png[Dark Theme Example]
NOTE: You can change the default theme in the *Advanced* section of the *Settings* tab.
[float]
[[dash-autorefresh]]
@ -67,8 +72,7 @@ in your Web page.
NOTE: A user must have Kibana access in order to view embedded dashboards.
Click the *Share* button to display HTML code to embed the dashboard in another Web page, along with a direct link to
the dashboard. Click the copy button image:images/Clipboard.png[Copy to Clipboard button] next to either option to copy
the code or the link to your clipboard.
the dashboard. You can select the text in either option to copy the code or the link to your clipboard.
[float]
[[embedding-dashboards]]

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/images/darktheme.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View file

@ -11,6 +11,8 @@ include::setup.asciidoc[]
include::getting-started.asciidoc[]
include::plugins.asciidoc[]
include::access.asciidoc[]
include::discover.asciidoc[]

122
docs/plugins.asciidoc Normal file
View file

@ -0,0 +1,122 @@
[[kibana-plugins]]
== Kibana Plugins
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
`installedPlugins` directory and unpacking the plugin files into a new directory.
[float]
=== Installing Plugins
Use the following command to install a plugin:
[source,shell]
bin/kibana plugin --install <org>/<package>/<version>
You can also use `-i` instead of `--install`, as in the following example:
[source,shell]
bin/kibana plugin -i elasticsearch/marvel-ui/latest
Because the organization given is `elasticsearch`, the plugin management tool automatically downloads the
plugin from `download.elastic.co`.
[float]
=== Installing Plugins from Github
When the specified plugin is not found at `download.elastic.co`, the plugin management tool parses the element
as a Github user name, as in the following example:
[source,shell]
bin/kibana plugin --install github-user/sample-plugin
Installing sample-plugin
Attempting to extract from https://download.elastic.co/github-user/sample-plugin/sample-plugin-latest.tar.gz
Attempting to extract from https://github.com/github-user/sample-plugin/archive/master.tar.gz
Downloading <some number> bytes....................
Extraction complete
Optimizing and caching browser bundles...
Plugin installation complete
[float]
=== Installing Plugins from an Arbitrary URL
You can specify a URL to a plugin with the `-u` or `--url` options after the `-i` or `--install` option, as in the
following example:
[source,shell]
bin/kibana plugin -i sample-plugin -u https://some.sample.url/directory
Installing sample-plugin
Attempting to extract from https://some.sample.url/directory
Downloading <some number> bytes....................
Extraction complete
Optimizing and caching browser bundles...
Plugin installation complete
You can specify URLs that use the HTTP, HTTPS, or `file` protocols.
[float]
=== Installing Plugins to an Arbitrary Directory
Use the `-d` or `--plugin-dir` option to specify a directory for plugins, as in the following example:
[source,shell]
bin/kibana plugin -i elasticsearch/sample-plugin/latest -d <path/to/directory>
Installing sample-plugin
Attempting to extract from https://download.elastic.co/elasticsearch/sample-plugin/sample-plugin-latest.tar.gz
Downloading <some number> bytes....................
Extraction complete
Optimizing and caching browser bundles...
Plugin installation complete
NOTE: This command creates the specified directory if it does not already exist.
[float]
=== Removing Plugins
Use the `--remove` or `-r` option to remove a plugin, including any configuration information, as in the following
example:
[source,shell]
bin/kibana plugin --remove marvel-ui
You can also remove a plugin manually by deleting the plugin's subdirectory under the `installedPlugins` directory.
[float]
=== Updating Plugins
To update a plugin, remove the current version and reinstall the plugin.
[float]
=== Configuring the Plugin Manager
By default, the plugin manager provides you with feedback on the status of the activity you've asked the plugin manager
to perform. You can control the level of feedback with the `--quiet` and `--silent` options. Use the `--quiet` option to
suppress all non-error output. Use the `--silent` option to suppress all output.
By default, plugin manager requests do not time out. Use the `--timeout` option, followed by a time, to change this
behavior, as in the following examples:
[source,shell]
.Waits for 30 seconds before failing
bin/kibana plugin --install username/sample-plugin --timeout 30s
[source,shell]
.Waits for 1 minute before failing
bin/kibana plugin --install username/sample-plugin --timeout 1m
[float]
==== Plugins and Custom Kibana Configurations
Use the `-c` or `--config` options to specify the path to the configuration file used to start Kibana. By default, Kibana
uses the configuration file `config/kibana.yml`. When you change your installed plugins, the `bin/kibana plugin` command
restarts the Kibana server. When you are using a customized configuration file, you must specify the
path to that configuration file each time you use the `bin/kibana plugin` command.
[float]
=== Plugin Manager Exit Codes
[horizontal]
0:: Success
64:: Unknown command or incorrect option parameter
74:: I/O error
70:: Other error

View file

@ -107,6 +107,18 @@ heatmap dots appear at full intensity.
* *Show Tooltip*: Check this box to have a tooltip with the values for a given dot when the cursor is on that dot.
*Desaturate map tiles*:: Desaturate the map's color in order to make the markers stand out more clearly.
*WMS compliant map server*:: Check this box to enable the use of a third-party mapping service that complies with the Web
Map Service (WMS) standard. Specify the following elements:
* *WMS url*: The URL for the WMS map service.
* *WMS layers*: A comma-separated list of the layers to use in this visualization. Each map server provides its own list of
layers.
* *WMS version*: The WMS version used by this map service.
* *WMS format*: The image format used by this map service. The two most common formats are `image/png` and `image/jpeg`.
* *WMS attribution*: An optional, user-defined string that identifies the map source. Maps display the attribution string
in the lower right corner.
* *WMS styles*: A comma-separated list of the styles to use in this visualization. Each map server provides its own styling
options.
After changing options, click the green *Apply changes* button to update your visualization, or the grey *Discard
changes* button to keep your visualization in its current state.

View file

@ -11,7 +11,7 @@
"dashboarding"
],
"private": false,
"version": "4.2.0-snapshot",
"version": "4.3.0-snapshot",
"build": {
"number": 8467,
"sha": "6cb7fec4e154faa0a4a3fee4b33dfef91b9870d9"
@ -43,132 +43,133 @@
"start": "./bin/kibana --dev",
"precommit": "grunt lintStagedFiles",
"karma": "karma start",
"elasticsearch": "grunt esvm:dev:keepalive"
"elasticsearch": "grunt esvm:dev:keepalive",
"lint": "grunt eslint:source",
"lintroller": "grunt eslint:fixSource"
},
"repository": {
"type": "git",
"url": "https://github.com/elastic/kibana.git"
},
"dependencies": {
"@spalger/angular-bootstrap": "^0.10.0",
"@spalger/angular-nvd3": "^1.0.0-beta",
"@spalger/filesaver": "^1.1.2",
"@spalger/leaflet-draw": "^0.2.3",
"@spalger/leaflet-heat": "^0.1.3",
"@spalger/nvd3": "^1.8.1",
"@spalger/ui-ace": "^0.2.3",
"@spalger/angular-bootstrap": "0.10.0",
"@spalger/angular-nvd3": "1.0.0-beta",
"@spalger/filesaver": "1.1.2",
"@spalger/leaflet-draw": "0.2.3",
"@spalger/leaflet-heat": "0.1.3",
"@spalger/numeral": "^2.0.0",
"@spalger/nvd3": "1.8.1",
"@spalger/ui-ace": "0.2.3",
"angular": "1.2.28",
"angular-bindonce": "0.3.1",
"angular-bootstrap-colorpicker": "^3.0.18",
"angular-elastic": "2.5.0",
"angular-route": "1.2.28",
"ansicolors": "^0.3.2",
"autoprefixer": "5.1.x",
"autoprefixer-loader": "2.0.x",
"babel": "^5.8.21",
"babel-core": "^5.8.22",
"babel-loader": "^5.3.2",
"babel-runtime": "^5.8.20",
"bluebird": "^2.9.27",
"boom": "^2.8.0",
"bootstrap": "^3.3.5",
"brace": "^0.5.1",
"bunyan": "^1.2.3",
"commander": "^2.8.1",
"css-loader": "^0.15.1",
"d3": "^3.5.6",
"elasticsearch": "^8.0.1",
"elasticsearch-browser": "^8.0.1",
"expiry-js": "^0.1.7",
"exports-loader": "^0.6.2",
"expose-loader": "^0.7.0",
"extract-text-webpack-plugin": "^0.8.2",
"file-loader": "^0.8.4",
"font-awesome": "^4.3.0",
"good": "^6.2.0",
"good-squeeze": "^2.1.0",
"gridster": "^0.5.6",
"hapi": "^8.6.1",
"imports-loader": "^0.6.4",
"jade": "^1.7.2",
"jade-loader": "^0.7.1",
"joi": "^6.4.3",
"jquery": "^2.1.4",
"js-yaml": "^3.2.5",
"json-stringify-safe": "^5.0.1",
"jstimezonedetect": "^1.0.5",
"leaflet": "^0.7.3",
"less": "^2.5.1",
"less-loader": "^2.2.0",
"lodash": "^3.10.0",
"ansicolors": "0.3.2",
"autoprefixer": "5.1.1",
"autoprefixer-loader": "2.0.0",
"babel": "5.8.23",
"babel-core": "5.8.23",
"babel-loader": "5.3.2",
"babel-runtime": "5.8.20",
"bluebird": "2.9.34",
"boom": "2.8.0",
"bootstrap": "3.3.5",
"brace": "0.5.1",
"bunyan": "1.4.0",
"commander": "2.8.1",
"css-loader": "0.17.0",
"d3": "3.5.6",
"elasticsearch": "8.0.1",
"elasticsearch-browser": "8.0.1",
"expiry-js": "0.1.7",
"exports-loader": "0.6.2",
"expose-loader": "0.7.0",
"extract-text-webpack-plugin": "0.8.2",
"file-loader": "0.8.4",
"font-awesome": "4.4.0",
"good": "6.3.0",
"good-squeeze": "2.1.0",
"gridster": "0.5.6",
"hapi": "8.8.1",
"imports-loader": "0.6.4",
"jade": "1.11.0",
"jade-loader": "0.7.1",
"joi": "6.6.1",
"jquery": "2.1.4",
"js-yaml": "3.4.1",
"json-stringify-safe": "5.0.1",
"jstimezonedetect": "1.0.5",
"leaflet": "0.7.5",
"less": "2.5.1",
"less-loader": "2.2.0",
"lodash": "3.10.1",
"marked": "0.3.3",
"minimatch": "^2.0.8",
"mkdirp": "^0.5.1",
"moment": "^2.10.3",
"ng-clip": "^0.2.6",
"numeral": "^1.5.3",
"raw-loader": "^0.5.1",
"request": "^2.60.0",
"requirefrom": "^0.2.0",
"rimraf": "^2.4.1",
"rjs-repack-loader": "^1.0.6",
"script-loader": "^0.6.1",
"semver": "^4.3.6",
"style-loader": "^0.12.3",
"tar": "^2.1.1",
"url-loader": "^0.5.6",
"webpack": "^1.10.0",
"webpack-directory-name-as-main": "^1.0.0",
"whatwg-fetch": "^0.9.0",
"zeroclipboard": "^2.2.0"
"minimatch": "2.0.10",
"mkdirp": "0.5.1",
"moment": "2.10.6",
"raw-loader": "0.5.1",
"request": "2.61.0",
"requirefrom": "0.2.0",
"rimraf": "2.4.3",
"rjs-repack-loader": "1.0.6",
"script-loader": "0.6.1",
"semver": "4.3.6",
"style-loader": "0.12.3",
"tar": "2.2.0",
"url-loader": "0.5.6",
"webpack": "1.12.1",
"webpack-directory-name-as-main": "1.0.0",
"whatwg-fetch": "0.9.0"
},
"devDependencies": {
"Nonsense": "^0.1.2",
"Nonsense": "0.1.2",
"angular-mocks": "1.2.28",
"auto-release-sinon": "^1.0.3",
"babel-eslint": "^4.1.1",
"chokidar": "^1.0.4",
"eslint": "^1.3.1",
"expect.js": "^0.3.1",
"faker": "^1.1.0",
"glob": "^4.3.2",
"grunt": "^0.4.5",
"grunt-babel": "^5.0.1",
"auto-release-sinon": "1.0.3",
"babel-eslint": "4.1.3",
"chokidar": "1.0.5",
"eslint": "1.5.1",
"eslint-plugin-mocha": "1.0.0",
"expect.js": "0.3.1",
"faker": "1.1.0",
"glob": "4.5.3",
"grunt": "0.4.5",
"grunt-babel": "5.0.1",
"grunt-cli": "0.1.13",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-copy": "^0.8.0",
"grunt-esvm": "^1.1.5",
"grunt-karma": "^0.12.0",
"grunt-run": "^0.4.0",
"grunt-s3": "^0.2.0-alpha.3",
"grunt-simple-mocha": "^0.4.0",
"gruntify-eslint": "^1.0.1",
"html-entities": "^1.1.1",
"husky": "^0.8.1",
"istanbul-instrumenter-loader": "^0.1.3",
"karma": "^0.13.3",
"karma-chrome-launcher": "^0.2.0",
"karma-coverage": "^0.5.0",
"karma-firefox-launcher": "^0.1.6",
"karma-growl-reporter": "^0.1.1",
"karma-ie-launcher": "^0.2.0",
"karma-mocha": "^0.2.0",
"karma-safari-launcher": "^0.1.1",
"libesvm": "^1.0.1",
"license-checker": "^3.1.0",
"load-grunt-config": "^0.7.0",
"marked-text-renderer": "^0.1.0",
"mocha": "^2.2.5",
"nock": "^2.9.0",
"npm": "3.2",
"portscanner": "^1.0.0",
"simple-git": "^1.3.0",
"sinon": "^1.15.4",
"source-map": "^0.4.4",
"wreck": "^6.1.0"
"grunt-contrib-clean": "0.6.0",
"grunt-contrib-copy": "0.8.1",
"grunt-esvm": "1.1.6",
"grunt-karma": "0.12.0",
"grunt-run": "0.4.0",
"grunt-s3": "0.2.0-alpha.3",
"grunt-simple-mocha": "0.4.0",
"gruntify-eslint": "1.0.1",
"html-entities": "1.1.3",
"husky": "0.8.1",
"istanbul-instrumenter-loader": "0.1.3",
"karma": "0.13.9",
"karma-chrome-launcher": "0.2.0",
"karma-coverage": "0.5.1",
"karma-firefox-launcher": "0.1.6",
"karma-growl-reporter": "0.1.1",
"karma-ie-launcher": "0.2.0",
"karma-mocha": "0.2.0",
"karma-safari-launcher": "0.1.1",
"libesvm": "1.0.7",
"license-checker": "3.1.0",
"load-grunt-config": "0.7.2",
"marked-text-renderer": "0.1.0",
"mocha": "2.3.0",
"nock": "2.10.0",
"npm": "2.11.0",
"portscanner": "1.0.0",
"simple-git": "1.8.0",
"sinon": "1.16.1",
"source-map": "0.4.4",
"wreck": "6.2.0"
},
"engines": {
"node": "2.5",
"npm": "3.2"
"node": "0.12",
"npm": "2.14.3"
}
}

View file

@ -31,8 +31,8 @@ ${help(this, ' ')}
};
Command.prototype.unknownArgv = function (argv) {
if (argv) this.__unkownArgv = argv;
return this.__unkownArgv ? this.__unkownArgv.slice(0) : [];
if (argv) this.__unknownArgv = argv;
return this.__unknownArgv ? this.__unknownArgv.slice(0) : [];
};
/**

View file

@ -23,7 +23,7 @@ program
.description('Get the help for a specific command')
.action(function (cmdName) {
var cmd = _.find(program.commands, { _name: cmdName });
if (!cmd) return this.error(`unknown command ${cmd}`);
if (!cmd) return this.error(`unknown command ${cmdName}`);
cmd.help();
});

View file

@ -34,6 +34,11 @@ module.exports = function (program) {
.option('-q, --quiet', 'Disable all process messaging except errors')
.option('-s, --silent', 'Disable all process messaging')
.option('-u, --url <url>', 'Specify download url')
.option(
'-c, --config <path>',
'Path to the config file',
fromRoot('config/kibana.yml')
)
.option(
'-t, --timeout <duration>',
'Length of time before failing; 0 for never fail',

View file

@ -1,5 +1,10 @@
let _ = require('lodash');
var utils = require('requirefrom')('src/utils');
var fromRoot = utils('fromRoot');
var pluginDownloader = require('./pluginDownloader');
var pluginCleaner = require('./pluginCleaner');
var KbnServer = require('../../server/KbnServer');
var readYamlConfig = require('../serve/readYamlConfig');
var fs = require('fs');
module.exports = {
@ -25,7 +30,36 @@ function install(settings, logger) {
.then(function () {
return downloader.download();
})
.then(function (curious) {
.then(async function() {
logger.log('Optimizing and caching browser bundles...');
let serverConfig = _.merge(
readYamlConfig(settings.config),
{
env: 'production',
logging: {
silent: settings.silent,
quiet: !settings.silent,
verbose: false
},
optimize: {
useBundleCache: false
},
server: {
autoListen: false
},
plugins: {
initialize: false,
scanDirs: [settings.pluginDir, fromRoot('src/plugins')],
paths: [settings.workingPath]
}
}
);
let kbnServer = new KbnServer(serverConfig);
await kbnServer.ready();
await kbnServer.close();
})
.then(function () {
fs.renameSync(settings.workingPath, settings.pluginPath);
logger.log('Plugin installation complete');
})

View file

@ -54,6 +54,10 @@ module.exports = function (options) {
settings.urls.push(options.url);
}
if (options.config) {
settings.config = options.config;
}
if (options.install) {
settings.action = 'install';
parts = options.install.split('/');

View file

@ -32,7 +32,10 @@ module.exports = function (program) {
.description('Run the kibana server')
.collectUnknownOptions()
.option('-e, --elasticsearch <uri>', 'Elasticsearch instance')
.option('-c, --config <path>', 'Path to the config file')
.option(
'-c, --config <path>',
'Path to the config file',
fromRoot('config/kibana.yml'))
.option('-p, --port <port>', 'The port to bind to', parseInt)
.option('-q, --quiet', 'Prevent all logging except errors')
.option('-Q, --silent', 'Prevent all logging')
@ -76,7 +79,7 @@ module.exports = function (program) {
let readYamlConfig = require('./readYamlConfig');
let KbnServer = src('server/KbnServer');
let settings = readYamlConfig(opts.config || fromRoot('config/kibana.yml'));
let settings = readYamlConfig(opts.config);
if (opts.dev) {
try { _.merge(settings, readYamlConfig(fromRoot('config/kibana.dev.yml'))); }

View file

@ -85,7 +85,6 @@ class BaseOptimizer {
new DirectoryNameAsMain()
]),
new webpack.NoErrorsPlugin(),
new webpack.optimize.DedupePlugin(),
new ExtractTextPlugin('[name].style.css', {
allChunks: true
}),
@ -101,7 +100,7 @@ class BaseOptimizer {
test: /\.less$/,
loader: ExtractTextPlugin.extract(
'style',
`css${mapQ}!autoprefixer?{ "browsers": ["last 2 versions","> 5%"] }!less${mapQ}`
`css${mapQ}!autoprefixer${mapQ ? mapQ + '&' : '?'}{ "browsers": ["last 2 versions","> 5%"] }!less${mapQ}`
)
},
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style', `css${mapQ}`) },

View file

@ -19,7 +19,9 @@ module.exports = async (kbnServer, server, config) => {
await bundles.writeEntryFiles();
// in prod, only bundle what looks invalid or missing
if (config.get('env.prod')) bundles = await kbnServer.bundles.getInvalidBundles();
if (config.get('optimize.useBundleCache')) {
bundles = await bundles.getInvalidBundles();
}
// we might not have any work to do
if (!bundles.getIds().length) {

View file

@ -29,7 +29,7 @@ module.exports = async (kbnServer, server, config) => {
break;
default:
throw new Error(`unkown kbnWorkerType "${process.env.kbnWorkerType}"`);
throw new Error(`unknown kbnWorkerType "${process.env.kbnWorkerType}"`);
}
};

View file

@ -1,12 +0,0 @@
module.exports = function (kibana) {
return new kibana.Plugin({
uiExports: {
app: {
id: 'appSwitcher',
main: 'plugins/appSwitcher/appSwitcher',
hidden: true,
autoload: kibana.autoload.styles
}
}
});
};

View file

@ -1,4 +0,0 @@
{
"name": "appSwitcher",
"version": "1.0.0"
}

View file

@ -1,16 +0,0 @@
<div ng-if="switcher.loading">
<div class="spinner large"></div>
</div>
<div ng-if="!switcher.loading" class="app-links">
<a
ng-repeat="app in switcher.apps"
ng-href="/app/{{app.id}}"
class="app-link">
<span ng-style="{ 'background-image': 'url(' + app.icon + ')' }" class="app-icon"></span>
<span class="app-info">
<h2 class="app-title">{{ app.title }}</h2>
<h3 class="app-description">{{ app.description }}</h3>
</span>
</a>
</li>

View file

@ -1,31 +0,0 @@
require('plugins/appSwitcher/appSwitcher.less');
var kibanaLogoUrl = require('ui/images/kibana.png');
require('ui/chrome')
.setBrand({
'logo': 'url(' + kibanaLogoUrl + ') left no-repeat',
'smallLogo': 'url(' + kibanaLogoUrl + ') left no-repeat'
})
.setShowAppsLink(false)
.setTabs([
{
id: '',
title: 'Apps',
activeIndicatorColor: '#ecf0f1'
}
])
.setRootTemplate(require('plugins/appSwitcher/appSwitcher.html'))
.setRootController('switcher', function SwitcherController($http) {
var switcher = {
loading: true
};
$http.get('/api/apps')
.then(function (resp) {
switcher.loading = false;
switcher.apps = resp.data;
});
return switcher;
});

View file

@ -1,43 +0,0 @@
@import (reference) "~ui/styles/variables";
.application {
background-color: @gray-lighter;
}
.app-links {
width: 700px;
margin: 25px auto;
text-align: justify;
.app-link {
display: inline-block;
vertical-align: top;
text-align: left;
background: white;
width: 200px;
margin: 0 30px 30px 0;
.app-icon {
display: block;
height: 200px;
background-size: cover;
background-position: center;
}
.app-info {
display: block;
padding: 15px;
}
.app-title {
margin: 0 0 10px;
color: @text-color;
}
.app-description {
font-size: 1em;
color: @gray;
margin: 0;
}
}
}

View file

@ -4,7 +4,6 @@
<div class="form-group">
<label>
Vis State
<kbn-clipboard copy="visStateJson"></kbn-clipboard>
</label>
<pre>{{visStateJson}}</pre>
</div>

View file

@ -3,8 +3,6 @@ define(function (require) {
require('ui/registry/spy_modes').register(VisDetailsSpyProvider);
function VisDetailsSpyProvider(Notifier, $filter, $rootScope, config) {
require('ui/clipboard');
return {
name: 'debug',
display: 'Debug',

View file

@ -1,42 +0,0 @@
var portscanner = require('portscanner');
var path = require('path');
var Promise = require('bluebird');
var libesvm = require('libesvm');
var fromRoot = require('requirefrom')('src/utils')('fromRoot');
function startEs() {
var options = {
branch: 'master',
directory: fromRoot('esvm/test-es'),
purge: true,
config: {
'cluster.name': 'test',
'network.host': '127.0.0.1'
}
};
var cluster = libesvm.createCluster(options);
return cluster.install().then(function () {
return cluster.start();
}).then(function () {
after(function () {
this.timeout(120000);
return cluster.shutdown();
});
return cluster;
});
}
function maybeStartES() {
return new Promise(function (resolve, reject) {
portscanner.checkPortStatus(9200, '127.0.0.1', function (err, status) {
if (err) return reject(err);
if (status === 'closed') return startEs().then(resolve, reject);
resolve();
});
});
}
module.exports = function () {
this.timeout(120000);
return maybeStartES();
};

View file

@ -8,7 +8,6 @@ var fromRoot = src('utils/fromRoot');
describe('plugins/elasticsearch', function () {
describe('routes', function () {
before(require('./_ensure_elasticsearch'));
var kbnServer;

View file

@ -0,0 +1,20 @@
const _ = require('lodash');
const Promise = require('bluebird');
const Boom = require('boom');
module.exports = (client) => {
return (req, endpoint, params = {}) => {
if (req.headers.authorization) {
_.set(params, 'headers.authorization', req.headers.authorization);
}
const api = _.get(client, endpoint);
if (!api) throw new Error(`callWithRequest called with an invalid endpoint: ${endpoint}`);
return api.call(client, params)
.catch((err) => {
if (err.status === 401) {
const options = { realm: 'Authorization Required' };
return Promise.reject(Boom.unauthorized(err.body, 'Basic', options));
}
return Promise.reject(err);
});
};
};

View file

@ -3,51 +3,70 @@ var _ = require('lodash');
var fs = require('fs');
var util = require('util');
var url = require('url');
var callWithRequest = require('./call_with_request');
module.exports = function (server) {
var config = server.config();
var uri = url.parse(config.get('elasticsearch.url'));
var username = config.get('elasticsearch.username');
var password = config.get('elasticsearch.password');
var verifySsl = config.get('elasticsearch.ssl.verify');
var clientCrt = config.get('elasticsearch.ssl.cert');
var clientKey = config.get('elasticsearch.ssl.key');
var ca = config.get('elasticsearch.ssl.ca');
var apiVersion = config.get('elasticsearch.apiVersion');
if (username && password) {
uri.auth = util.format('%s:%s', username, password);
}
function createClient(options) {
options = _.defaults(options || {}, {
url: config.get('elasticsearch.url'),
username: config.get('elasticsearch.username'),
password: config.get('elasticsearch.password'),
verifySsl: config.get('elasticsearch.ssl.verify'),
clientCrt: config.get('elasticsearch.ssl.cert'),
clientKey: config.get('elasticsearch.ssl.key'),
ca: config.get('elasticsearch.ssl.ca'),
apiVersion: config.get('elasticsearch.apiVersion'),
keepAlive: true,
auth: true
});
var ssl = { rejectUnauthorized: verifySsl };
if (clientCrt && clientKey) {
ssl.cert = fs.readFileSync(clientCrt, 'utf8');
ssl.key = fs.readFileSync(clientKey, 'utf8');
}
if (ca) {
ssl.ca = fs.readFileSync(ca, 'utf8');
}
var uri = url.parse(options.url);
var client = new elasticsearch.Client({
host: url.format(uri),
ssl: ssl,
apiVersion: apiVersion,
log: function () {
this.error = function (err) {
server.log(['error', 'elasticsearch'], err);
};
this.warning = function (message) {
server.log(['warning', 'elasticsearch'], message);
};
this.info = _.noop;
this.debug = _.noop;
this.trace = _.noop;
this.close = _.noop;
var authorization;
if (options.auth && options.username && options.password) {
uri.auth = util.format('%s:%s', options.username, options.password);
}
});
var ssl = { rejectUnauthorized: options.verifySsl };
if (options.clientCrt && options.clientKey) {
ssl.cert = fs.readFileSync(options.clientCrt, 'utf8');
ssl.key = fs.readFileSync(options.clientKey, 'utf8');
}
if (options.ca) {
ssl.ca = fs.readFileSync(options.ca, 'utf8');
}
return new elasticsearch.Client({
host: url.format(uri),
ssl: ssl,
apiVersion: options.apiVersion,
keepAlive: options.keepAlive,
log: function () {
this.error = function (err) {
server.log(['error', 'elasticsearch'], err);
};
this.warning = function (message) {
server.log(['warning', 'elasticsearch'], message);
};
this.info = _.noop;
this.debug = _.noop;
this.trace = _.noop;
this.close = _.noop;
}
});
}
var client = createClient();
server.on('close', _.bindKey(client, 'close'));
var noAuthClient = createClient({ auth: false });
server.on('close', _.bindKey(noAuthClient, 'close'));
server.expose('client', client);
server.expose('createClient', createClient);
server.expose('callWithRequest', callWithRequest(noAuthClient));
return client;

View file

@ -5,8 +5,10 @@ module.exports = function mapUri(server, prefix) {
return function (request, done) {
var path = request.path.replace('/elasticsearch', '');
var url = config.get('elasticsearch.url');
if (!/\/$/.test(url)) url += '/';
if (path) url = resolve(url, path);
if (path) {
if (/\/$/.test(url)) url = url.substring(0, url.length - 1);
url += path;
}
var query = querystring.stringify(request.query);
if (query) url += '?' + query;
done(null, url);

View file

@ -21,8 +21,8 @@
ng-model="vis.params.yAxis.max"
ng-required="vis.params.setYExtents">
</label>
<div ng-show="vis.params.yAxis.min > vis.params.yAxis.max">
<span class="text-danger">Min must not exceed max</span>
<div ng-show="vis.params.yAxis.min >= vis.params.yAxis.max">
<span class="text-danger">Max must be greater than min</span>
</div>
<label>
y-min

View file

@ -13,7 +13,7 @@ module.exports = function (kibana) {
app: {
title: 'Kibana',
description: 'the kibana you know and love',
icon: 'plugins/kibana/settings/sections/about/barcode.svg',
//icon: 'plugins/kibana/settings/sections/about/barcode.svg',
main: 'plugins/kibana/kibana',
uses: [
'visTypes',

View file

@ -73,6 +73,16 @@
<i aria-hidden="true" class="fa fa-plus-circle"></i>
</button>
</kbn-tooltip>
<kbn-tooltip text="Options" placement="bottom" append-to-body="1">
<button
aria-label="Options"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('options') }}"
ng-class="{active: configTemplate.is('options')}"
ng-click="configTemplate.toggle('options');">
<i aria-hidden="true" class="fa fa-gear"></i>
</button>
</kbn-tooltip>
</div>
</navbar>

View file

@ -3,13 +3,13 @@ define(function (require) {
var $ = require('jquery');
var angular = require('angular');
var ConfigTemplate = require('ui/ConfigTemplate');
var chrome = require('ui/chrome');
require('ui/directives/config');
require('ui/courier');
require('ui/config');
require('ui/notify');
require('ui/typeahead');
require('ui/clipboard');
require('plugins/kibana/dashboard/directives/grid');
require('plugins/kibana/dashboard/components/panel/panel');
@ -32,7 +32,7 @@ define(function (require) {
.when('/dashboard', {
template: require('plugins/kibana/dashboard/index.html'),
resolve: {
dash: function (savedDashboards) {
dash: function (savedDashboards, config) {
return savedDashboards.get();
}
}
@ -51,7 +51,7 @@ define(function (require) {
app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, kbnUrl) {
return {
controller: function ($scope, $route, $routeParams, $location, Private, getAppState) {
controller: function ($scope, $rootScope, $route, $routeParams, $location, Private, getAppState) {
var queryFilter = Private(require('ui/filter_bar/query_filter'));
@ -80,17 +80,23 @@ define(function (require) {
var stateDefaults = {
title: dash.title,
panels: dash.panelsJSON ? JSON.parse(dash.panelsJSON) : [],
options: dash.optionsJSON ? JSON.parse(dash.optionsJSON) : {},
query: extractQueryFromFilters(dash.searchSource.getOwn('filter')) || {query_string: {query: '*'}},
filters: _.reject(dash.searchSource.getOwn('filter'), matchQueryFilter)
filters: _.reject(dash.searchSource.getOwn('filter'), matchQueryFilter),
};
var $state = $scope.state = new AppState(stateDefaults);
$scope.$watchCollection('state.options', function (newVal, oldVal) {
if (!angular.equals(newVal, oldVal)) $state.save();
});
$scope.$watch('state.options.darkTheme', setDarkTheme);
$scope.configTemplate = new ConfigTemplate({
save: require('plugins/kibana/dashboard/partials/save_dashboard.html'),
load: require('plugins/kibana/dashboard/partials/load_dashboard.html'),
share: require('plugins/kibana/dashboard/partials/share.html'),
pickVis: require('plugins/kibana/dashboard/partials/pick_visualization.html')
pickVis: require('plugins/kibana/dashboard/partials/pick_visualization.html'),
options: require('plugins/kibana/dashboard/partials/options.html')
});
$scope.refresh = _.bindKey(courier, 'fetch');
@ -123,6 +129,12 @@ define(function (require) {
}
}
function setDarkTheme(enabled) {
var theme = Boolean(enabled) ? 'theme-dark' : 'theme-light';
chrome.removeApplicationClass(['theme-dark', 'theme-light']);
chrome.addApplicationClass(theme);
}
// update root source when filters update
$scope.$listen(queryFilter, 'update', function () {
updateQueryOnRootSource();
@ -148,6 +160,7 @@ define(function (require) {
dash.panelsJSON = angular.toJson($state.panels);
dash.timeFrom = dash.timeRestore ? timefilter.time.from : undefined;
dash.timeTo = dash.timeRestore ? timefilter.time.to : undefined;
dash.optionsJSON = angular.toJson($state.options);
dash.save()
.then(function (id) {
@ -191,6 +204,7 @@ define(function (require) {
// Setup configurable values for config directive, after objects are initialized
$scope.opts = {
dashboard: dash,
ui: $state.options,
save: $scope.save,
addVis: $scope.addVis,
addSearch: $scope.addSearch,

View file

@ -0,0 +1,10 @@
<form role="form" class="options">
<p>
<div class="input-group">
<label>
<input type="checkbox" ng-model="opts.ui.darkTheme" ng-checked="opts.ui.darkTheme">
Use dark theme
</label>
</div>
</p>
</form>

View file

@ -4,11 +4,11 @@
<li ng-class="{active: mode == 'search'}"><a ng-click="mode='search'" title="Searches">Searches</a></li>
</ul>
<li class="list-group-item" ng-switch-when="visualization">
<div class="list-group-item" ng-switch-when="visualization">
<saved-object-finder title="Choose a visualization" type="visualizations" on-choose="opts.addVis"></saved-object-finder>
</li>
</div>
<li class="list-group-item" ng-switch-when="search">
<div class="list-group-item" ng-switch-when="search">
<saved-object-finder title="Choose a search" type="searches" on-choose="opts.addSearch"></saved-object-finder>
</li>
</div>
</div>
</div>

View file

@ -3,8 +3,7 @@
<p>
<div class="input-group">
<label>
Embed this dashboard.
<kbn-clipboard copy="opts.shareData().embed"></kbn-clipboard>
Embed this dashboard
<small>Add to your html source. Note all clients must still be able to access kibana</small>
</label>
<div class="form-control" disabled>{{opts.shareData().embed}}</div>
@ -15,7 +14,6 @@
<div class="input-group">
<label>
Share a link
<kbn-clipboard copy="opts.shareData().link"></kbn-clipboard>
</label>
<div class="form-control" disabled>{{opts.shareData().link}}</div>
</div>

View file

@ -1,11 +1,11 @@
define(function (require) {
var module = require('ui/modules').get('app/dashboard');
var angular = require('angular');
var _ = require('lodash');
var moment = require('moment');
// Used only by the savedDashboards service, usually no reason to change this
module.factory('SavedDashboard', function (courier) {
module.factory('SavedDashboard', function (courier, config) {
// SavedDashboard constructor. Usually you'd interact with an instance of this.
// ID is option, without it one will be generated on save.
_.class(SavedDashboard).inherits(courier.SavedObject);
@ -25,10 +25,13 @@ define(function (require) {
hits: 0,
description: '',
panelsJSON: '[]',
optionsJSON: angular.toJson({
darkTheme: config.get('dashboard:defaultDarkTheme')
}),
version: 1,
timeRestore: false,
timeTo: undefined,
timeFrom: undefined
timeFrom: undefined,
},
// if an indexPattern was saved with the searchsource of a SavedDashboard
@ -46,10 +49,11 @@ define(function (require) {
hits: 'integer',
description: 'string',
panelsJSON: 'string',
optionsJSON: 'string',
version: 'integer',
timeRestore: 'boolean',
timeTo: 'string',
timeFrom: 'string'
timeFrom: 'string',
};
SavedDashboard.searchsource = true;

View file

@ -43,7 +43,7 @@ define(function (require) {
};
this.find = function (searchString) {
this.find = function (searchString, size = 100) {
var self = this;
var body;
if (searchString) {
@ -64,7 +64,7 @@ define(function (require) {
index: kbnIndex,
type: 'dashboard',
body: body,
size: 100
size: size
})
.then(function (resp) {
return {

View file

@ -1,9 +1,8 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@dashboard-background: @gray-lighter;
.tab-dashboard {
background-color: @dashboard-background;
background-color: @dashboard-bg;
}
dashboard-grid {
@ -14,14 +13,14 @@ dashboard-grid {
.start-screen {
margin: 20px;
background-color: @gray-lighter;
background-color: @dashboard-bg;
padding: 20px;
}
.gridster {
list-style-type: none;
display: block;
background-color: @dashboard-background;
background-color: @dashboard-bg;
margin: 0;
padding: 0;
@ -41,8 +40,8 @@ dashboard-grid {
dashboard-panel {
display: block;
height: 100%;
background: @panel-bg;
color: @text-color;
background: @dashboard-panel-bg;
color: @dashboard-panel-color;
padding: 0;
overflow: hidden;
position: relative;
@ -61,6 +60,8 @@ dashboard-grid {
flex: 0 0 auto;
white-space: nowrap;
display: flex;
border-top-right-radius: 0;
border-top-left-radius: 0;
div.btn-group {
white-space: nowrap;
@ -85,12 +86,12 @@ dashboard-grid {
}
a {
color: @text-color;
color: @dashboard-panel-heading-link-color;
border: none;
background: none;
padding: 0px 3px;
&:hover {
color: @link-hover-color;
color: @dashboard-panel-heading-link-hover-color;
}
}
}
@ -105,7 +106,7 @@ dashboard-grid {
.fa-exclamation-triangle {
font-size: 2em;
color: @btn-danger-bg;
color: @dashboard-panel-load-error-color;
}
}

View file

@ -46,7 +46,7 @@ define(function (require) {
});
};
this.find = function (searchString) {
this.find = function (searchString, size = 100) {
var self = this;
var body;
if (searchString) {
@ -67,7 +67,7 @@ define(function (require) {
index: kbnIndex,
type: 'search',
body: body,
size: 100
size: size
})
.then(function (resp) {
return {

View file

@ -8,6 +8,7 @@
&-wrapper {
padding-right: 0px !important;
padding-left: @collapser-width;
}
&-content {
@ -61,16 +62,16 @@
opacity: 0.75;
text-align: center;
background-color: #ffffff;
background-color: transparent;
// spinner and prorgess counter
// spinner and progress counter
> div {
text-align: center;
}
}
&-info {
background-color: @well-bg;
background-color: @discover-info-bg;
float: right;
padding: 5px 10px;
border-bottom-left-radius: @border-radius-base;
@ -94,7 +95,7 @@
}
&-footer {
background-color: @well-bg;
background-color: @discover-table-footer-bg;
padding: 5px 10px;
}
@ -116,7 +117,7 @@
&-field {
&-filter {
background-color: @well-bg;
background-color: @discover-field-filter-bg;
margin-right: 10px;
.form-group {
@ -126,16 +127,17 @@
}
&-toggle {
color: @btn-success-color;
color: @discover-field-toggle-color;
font-size: 9px;
}
}
}
.shard-failures {
color: @brand-danger;
background-color: lighten(@brand-danger, 40%) !important;
border: 1px solid @brand-danger;
color: @discover-shard-failures-color;
background-color: @discover-shard-failures-bg !important;
border: 1px solid;
border-color: @discover-shard-failures-border;
border-radius: @border-radius-base;
padding: 0 20px 20px;
li {
@ -157,19 +159,21 @@
}
.discover-field-details {
border-top: 1px solid @well-border;
border-top: 1px solid;
border-top-color: @discover-field-details-border;
padding: 5px 10px;
background-color: @body-bg;
color: @text-color;
background-color: @discover-field-details-bg;
color: @discover-field-details-color;
&-close {
text-align: center;
border-top: 1px solid @well-border;
background-color: @well-bg;
border-top: 1px solid;
border-color: @discover-field-details-close-border;
background-color: @discover-field-details-close-bg;
&:hover {
background-color: @sidebar-hover-bg;
color: @sidebar-hover-color;
background-color: @discover-field-details-close-hover-bg;
color: @discover-field-details-close-hover-color;
}
}
@ -190,11 +194,11 @@
}
a {
color: @link-color !important;
color: @discover-link-color !important;
}
a.btn {
color: @text-color !important;
color: @discover-link-btn-color !important;
}
.progress {
@ -208,7 +212,7 @@
border-radius: 4px;
span {
background-color: @gray;
background-color: @discover-field-details-progress-bar-bg;
padding: 3px;
border-radius: 4px;
margin-left: 3px;
@ -225,11 +229,11 @@ field-name,
text-align: center;
display: inline-block;
width: 12px;
color: @text-muted;
color: @field-name-color;
}
&.no-results {
color: @text-muted;
color: @field-name-no-results-color;
}
}

View file

@ -23,7 +23,7 @@ chrome
.setNavBackground('#222222')
.setTabDefaults({
resetWhenActive: true,
trackLastPath: true,
lastUrlStore: window.sessionStore,
activeIndicatorColor: '#656a76'
})
.setTabs([
@ -47,10 +47,9 @@ chrome
title: 'Settings'
}
])
.setRootController('kibana', function ($scope, courier) {
.setRootController('kibana', function ($scope, $rootScope, courier, config) {
// wait for the application to finish loading
$scope.$on('application.load', function () {
courier.start();
});
});

View file

@ -13,7 +13,7 @@
height="64"
id="svg5235"
version="1.1"
inkscape:version="0.91+devel+osxmenu r12835"
inkscape:version="0.91 r13725"
viewBox="0 0 64 64"
sodipodi:docname="barcode.svg">
<defs
@ -45,7 +45,7 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
@ -54,404 +54,401 @@
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<g
id="g6209"
transform="matrix(0.57440105,0,0,0.57440105,-36.987859,-56.970747)">
<rect
y="104.33428"
x="97.825981"
height="100.54836"
width="2.4159346"
id="rect5251-3"
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="85.735123"
height="100.54836"
width="7.5297971"
id="rect5251-9"
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="75.889793"
height="100.54836"
width="5.0022545"
id="rect5251-8"
style="opacity:1;fill:#f3bb19;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="72.783882"
height="100.54836"
width="3.2287681"
id="rect5251-2"
style="opacity:1;fill:#2d448e;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="69.63768"
height="100.54836"
width="3.159905"
id="rect5251"
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="119.37186"
height="100.54836"
width="6.9419966"
id="rect5251-9-9"
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="131.24544"
height="100.54836"
width="4.9434743"
id="rect5251-5-9"
style="opacity:1;fill:#006555;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="83.986267"
height="100.54836"
width="1.8869137"
id="rect5251-5-0"
style="opacity:1;fill:#006555;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="93.214729"
height="100.54836"
width="4.7083545"
id="rect5251-7"
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="136.24173"
height="100.54836"
width="5.8251753"
id="rect5251-36"
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="80.870918"
height="100.54836"
width="3.1212945"
id="rect5251-3-7"
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="100.15076"
height="100.54836"
width="3.5915353"
id="rect5251-9-9-3"
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="166.68982"
height="100.54836"
width="3.9442151"
id="rect5251-9-9-8"
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="110.55486"
height="100.54836"
width="3.3564136"
id="rect5251-3-3"
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33429"
x="148.82066"
height="100.54836"
width="4.1793332"
id="rect5251-3-0"
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33427"
x="129.07057"
height="100.54838"
width="2.1808138"
id="rect5251-8-3"
style="opacity:1;fill:#f3bb19;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="142.00218"
height="100.54836"
width="3.0037344"
id="rect5251-8-33"
style="opacity:1;fill:#f3bb19;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="106.20512"
height="100.54836"
width="4.3556747"
id="rect5251-7-3"
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="103.73636"
height="100.54836"
width="2.4159336"
id="rect5251-2-0"
style="opacity:1;fill:#2d448e;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="116.25652"
height="100.54836"
width="3.8266549"
id="rect5251-36-7"
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33427"
x="113.9641"
height="100.54838"
width="2.2983737"
id="rect5251-5-3"
style="opacity:1;fill:#006555;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="126.30791"
height="100.54836"
width="2.8273947"
id="rect5251-3-7-7"
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="144.8824"
height="100.54836"
width="2.0632553"
id="rect5251-3-7-70"
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="146.99847"
height="100.54836"
width="1.8869162"
id="rect5251-9-9-8-4"
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="158.32114"
height="100.54836"
width="4.708354"
id="rect5251-3-7-70-2"
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33427"
x="152.97215"
height="100.54838"
width="5.413713"
id="rect5251-8-339-2"
style="opacity:1;fill:#f3bb19;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="104.33428"
x="162.99417"
height="100.54836"
width="3.8266544"
id="rect5251-36-5"
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
id="g4300"
transform="matrix(1.1002186,0,0,1.1002186,-3.2119331,-3.0092752)">
<g
transform="matrix(0.57440105,0,0,0.57440105,-36.987859,-56.970747)"
id="g6209">
<rect
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-3"
width="2.4159346"
height="100.54836"
x="97.825981"
y="104.33428" />
<rect
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-9"
width="7.5297971"
height="100.54836"
x="85.735123"
y="104.33428" />
<rect
style="opacity:1;fill:#f3bb19;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-8"
width="5.0022545"
height="100.54836"
x="75.889793"
y="104.33428" />
<rect
style="opacity:1;fill:#2d448e;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-2"
width="3.2287681"
height="100.54836"
x="72.783882"
y="104.33428" />
<rect
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251"
width="3.159905"
height="100.54836"
x="69.63768"
y="104.33428" />
<rect
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-9-9"
width="6.9419966"
height="100.54836"
x="119.37186"
y="104.33428" />
<rect
style="opacity:1;fill:#006555;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-5-9"
width="4.9434743"
height="100.54836"
x="131.24544"
y="104.33428" />
<rect
style="opacity:1;fill:#006555;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-5-0"
width="1.8869137"
height="100.54836"
x="83.986267"
y="104.33428" />
<rect
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-7"
width="4.7083545"
height="100.54836"
x="93.214729"
y="104.33428" />
<rect
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-36"
width="5.8251753"
height="100.54836"
x="136.24173"
y="104.33428" />
<rect
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-3-7"
width="3.1212945"
height="100.54836"
x="80.870918"
y="104.33428" />
<rect
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-9-9-3"
width="3.5915353"
height="100.54836"
x="100.15076"
y="104.33428" />
<rect
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-9-9-8"
width="3.9442151"
height="100.54836"
x="166.68982"
y="104.33428" />
<rect
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-3-3"
width="3.3564136"
height="100.54836"
x="110.55486"
y="104.33428" />
<rect
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-3-0"
width="4.1793332"
height="100.54836"
x="148.82066"
y="104.33429" />
<rect
style="opacity:1;fill:#f3bb19;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-8-3"
width="2.1808138"
height="100.54838"
x="129.07057"
y="104.33427" />
<rect
style="opacity:1;fill:#f3bb19;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-8-33"
width="3.0037344"
height="100.54836"
x="142.00218"
y="104.33428" />
<rect
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-7-3"
width="4.3556747"
height="100.54836"
x="106.20512"
y="104.33428" />
<rect
style="opacity:1;fill:#2d448e;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-2-0"
width="2.4159336"
height="100.54836"
x="103.73636"
y="104.33428" />
<rect
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-36-7"
width="3.8266549"
height="100.54836"
x="116.25652"
y="104.33428" />
<rect
style="opacity:1;fill:#006555;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-5-3"
width="2.2983737"
height="100.54838"
x="113.9641"
y="104.33427" />
<rect
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-3-7-7"
width="2.8273947"
height="100.54836"
x="126.30791"
y="104.33428" />
<rect
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-3-7-70"
width="2.0632553"
height="100.54836"
x="144.8824"
y="104.33428" />
<rect
style="opacity:1;fill:#ea458b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-9-9-8-4"
width="1.8869162"
height="100.54836"
x="146.99847"
y="104.33428" />
<rect
style="opacity:1;fill:#39bdb1;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-3-7-70-2"
width="4.708354"
height="100.54836"
x="158.32114"
y="104.33428" />
<rect
style="opacity:1;fill:#f3bb19;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-8-339-2"
width="5.413713"
height="100.54838"
x="152.97215"
y="104.33427" />
<rect
style="opacity:1;fill:#83cb1c;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5251-36-5"
width="3.8266544"
height="100.54836"
x="162.99417"
y="104.33428" />
</g>
<g
transform="matrix(0.57440105,0,0,1.9251702,3.0622581,2.9589583)"
id="g4544">
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="1"
y1="0"
x2="1"
y2="30"
id="line4450" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="3.5"
y1="0"
x2="3.5"
y2="30"
id="line4452" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="6.5"
y1="0"
x2="6.5"
y2="30"
id="line4454" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="11.5"
y1="0"
x2="11.5"
y2="30"
id="line4456" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="14.5"
y1="0"
x2="14.5"
y2="30"
id="line4458" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="17"
y1="0"
x2="17"
y2="30"
id="line4460" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="23"
y1="0"
x2="23"
y2="30"
id="line4462" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="28.5"
y1="0"
x2="28.5"
y2="30"
id="line4464" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="30.5"
y1="0"
x2="30.5"
y2="30"
id="line4466" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="34"
y1="0"
x2="34"
y2="30"
id="line4468" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="37"
y1="0"
x2="37"
y2="30"
id="line4470" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="41"
y1="0"
x2="41"
y2="30"
id="line4472" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="44.5"
y1="0"
x2="44.5"
y2="30"
id="line4474" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="47"
y1="0"
x2="47"
y2="30"
id="line4476" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="50.5"
y1="0"
x2="50.5"
y2="30"
id="line4478" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="56"
y1="0"
x2="56"
y2="30"
id="line4480" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="59.5"
y1="0"
x2="59.5"
y2="30"
id="line4482" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="61.5"
y1="0"
x2="61.5"
y2="30"
id="line4484" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="67"
y1="0"
x2="67"
y2="30"
id="line4486" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="72.5"
y1="0"
x2="72.5"
y2="30"
id="line4488" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="75.5"
y1="0"
x2="75.5"
y2="30"
id="line4490" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="77.5"
y1="0"
x2="77.5"
y2="30"
id="line4492" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="79.5"
y1="0"
x2="79.5"
y2="30"
id="line4494" />
<line
style="fill:none;stroke:#000000;stroke-width:4"
x1="83"
y1="0"
x2="83"
y2="30"
id="line4496" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="89"
y1="0"
x2="89"
y2="30"
id="line4498" />
<line
style="fill:none;stroke:#000000;stroke-width:3"
x1="94.5"
y1="0"
x2="94.5"
y2="30"
id="line4500" />
<line
style="fill:none;stroke:#000000;stroke-width:1"
x1="97.5"
y1="0"
x2="97.5"
y2="30"
id="line4502" />
<line
style="fill:none;stroke:#000000;stroke-width:2"
x1="100"
y1="0"
x2="100"
y2="30"
id="line4504" />
</g>
</g>
<g
id="g4544"
transform="matrix(0.57440105,0,0,1.9251702,3.0622581,2.9589583)">
<line
id="line4450"
y2="30"
x2="1"
y1="0"
x1="1"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4452"
y2="30"
x2="3.5"
y1="0"
x1="3.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4454"
y2="30"
x2="6.5"
y1="0"
x1="6.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4456"
y2="30"
x2="11.5"
y1="0"
x1="11.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4458"
y2="30"
x2="14.5"
y1="0"
x1="14.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4460"
y2="30"
x2="17"
y1="0"
x1="17"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4462"
y2="30"
x2="23"
y1="0"
x1="23"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4464"
y2="30"
x2="28.5"
y1="0"
x1="28.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4466"
y2="30"
x2="30.5"
y1="0"
x1="30.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4468"
y2="30"
x2="34"
y1="0"
x1="34"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4470"
y2="30"
x2="37"
y1="0"
x1="37"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4472"
y2="30"
x2="41"
y1="0"
x1="41"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4474"
y2="30"
x2="44.5"
y1="0"
x1="44.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4476"
y2="30"
x2="47"
y1="0"
x1="47"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4478"
y2="30"
x2="50.5"
y1="0"
x1="50.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4480"
y2="30"
x2="56"
y1="0"
x1="56"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4482"
y2="30"
x2="59.5"
y1="0"
x1="59.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4484"
y2="30"
x2="61.5"
y1="0"
x1="61.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4486"
y2="30"
x2="67"
y1="0"
x1="67"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4488"
y2="30"
x2="72.5"
y1="0"
x1="72.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4490"
y2="30"
x2="75.5"
y1="0"
x1="75.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4492"
y2="30"
x2="77.5"
y1="0"
x1="77.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4494"
y2="30"
x2="79.5"
y1="0"
x1="79.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4496"
y2="30"
x2="83"
y1="0"
x1="83"
style="fill:none;stroke:#000000;stroke-width:4" />
<line
id="line4498"
y2="30"
x2="89"
y1="0"
x1="89"
style="fill:none;stroke:#000000;stroke-width:2" />
<line
id="line4500"
y2="30"
x2="94.5"
y1="0"
x1="94.5"
style="fill:none;stroke:#000000;stroke-width:3" />
<line
id="line4502"
y2="30"
x2="97.5"
y1="0"
x1="97.5"
style="fill:none;stroke:#000000;stroke-width:1" />
<line
id="line4504"
y2="30"
x2="100"
y1="0"
x1="100"
style="fill:none;stroke:#000000;stroke-width:2" />
</g>
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3.01796293;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6306"
width="61.028111"
height="60.597305"
x="1.5768572"
y="1.5882187" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -1,9 +1,12 @@
<tr ng-class="conf.value === undefined ? 'default' : 'custom'">
<td class="name">
<b>{{conf.name}}</b>
<span class="smaller" ng-show="conf.value !== undefined">
<span class="smaller" ng-show="!conf.isCustom && conf.value !== undefined">
(Default: <i>{{conf.defVal == undefined ? 'null' : conf.defVal}}</i>)
</span>
<span class="smaller" ng-show="conf.isCustom">
(Custom setting)
</span>
<br>
<span class="smaller" ng-bind-html="conf.description | trustAsHtml"></span>
</td>

View file

@ -2,9 +2,12 @@
<kbn-settings-advanced class="container">
<div class="bs-callout bs-callout-warning">
<h4>Caution: You can break stuff here</h4>
Be careful in here, these settings are for very advanced users only. Tweaks you make here can break large portions of Kibana.
Some of these settings may be undocumented, unsupported or experimental. Blanking a field will cause Kibana to use its internal
defaults which may be unacceptable given other configuration directives.
Be careful in here, these settings are for very advanced users only.
Tweaks you make here can break large portionsof Kibana. Some of these
settings may be undocumented, unsupported or experimental. If a field has
a default value, blanking the field will reset it to its default which
may be unacceptable given other configuration directives. Deleting a
custom setting will permanently remove it from Kibana's config.
</div>
<form role="form">
<input aria-label="Filter" ng-model="advancedFilter" class="form-control span12" type="text" placeholder="Filter"/>

View file

@ -1,6 +1,6 @@
define(function (require) {
var _ = require('lodash');
var getValType = require('plugins/kibana/settings/sections/advanced/lib/get_val_type');
var toEditableConfig = require('plugins/kibana/settings/sections/advanced/lib/to_editable_config');
require('plugins/kibana/settings/sections/advanced/advanced_row');
@ -20,46 +20,34 @@ define(function (require) {
ESC: 27
};
var NAMED_EDITORS = ['json', 'array', 'boolean', 'select'];
var NORMAL_EDITOR = ['number', 'string', 'null', 'undefined'];
function getEditorType(conf) {
if (_.contains(NAMED_EDITORS, conf.type)) return conf.type;
if (_.contains(NORMAL_EDITOR, conf.type)) return 'normal';
}
function isTypeComplex(conf) {
return !(conf.json || conf.array || conf.bool || conf.normal);
}
function notDefaultConfig(configName) {
return !(configName in configDefaults);
}
function readConfigVals() {
var configVals = config._vals();
$scope.configs = _.map(configDefaults, function (def, name) {
var val = configVals[name];
var conf = {
name: name,
defVal: def.value,
type: getValType(def, val),
description: def.description,
options: def.options,
value: val,
};
var customConfig = Object.keys(configVals)
.filter(notDefaultConfig)
.map(name => toEditableConfig(false, name, configVals[name]));
var editor = getEditorType(conf);
conf.json = editor === 'json';
conf.select = editor === 'select';
conf.bool = editor === 'boolean';
conf.array = editor === 'array';
conf.normal = editor === 'normal';
conf.tooComplex = !editor;
return conf;
});
$scope.configs = _(configDefaults)
.map((def, name) => toEditableConfig(def, name, configVals[name]))
.reject('readonly')
.concat(customConfig)
.value();
}
// react to changes of the config values
var unhook = $rootScope.$on('change:config', readConfigVals);
$scope.$on('$destroy', unhook);
// initial config setup
readConfigVals();
$rootScope.$on('change:config', readConfigVals);
}
};
});

View file

@ -0,0 +1,27 @@
var getEditorType = require('plugins/kibana/settings/sections/advanced/lib/get_editor_type');
var expect = require('expect.js');
describe('Settings', function () {
describe('Advanced', function () {
describe('getEditorType(conf)', function () {
context('when given type has a named editor', function () {
it('returns that named editor', function () {
expect(getEditorType({ type: 'json' })).to.equal('json');
expect(getEditorType({ type: 'array' })).to.equal('array');
expect(getEditorType({ type: 'boolean' })).to.equal('boolean');
expect(getEditorType({ type: 'select' })).to.equal('select');
});
});
context('when given a type of number, string, null, or undefined', function () {
it('returns "normal"', function () {
expect(getEditorType({ type: 'number' })).to.equal('normal');
expect(getEditorType({ type: 'string' })).to.equal('normal');
expect(getEditorType({ type: 'null' })).to.equal('normal');
expect(getEditorType({ type: 'undefined' })).to.equal('normal');
});
});
});
});
});

View file

@ -0,0 +1,86 @@
var toEditableConfig = require('plugins/kibana/settings/sections/advanced/lib/to_editable_config');
var expect = require('expect.js');
describe('Settings', function () {
describe('Advanced', function () {
describe('toEditableConfig(def, name, value)', function () {
it('sets name', function () {
expect(invoke({ name: 'who' }).name).to.equal('who');
});
it('sets value', function () {
expect(invoke({ value: 'what' }).value).to.equal('what');
});
it('sets type', function () {
expect(invoke({ value: 'what' }).type).to.be('string');
expect(invoke({ value: 0 }).type).to.be('number');
expect(invoke({ value: [] }).type).to.be('array');
});
context('when given a setting definition object', function () {
var def;
beforeEach(function () {
def = {
value: 'the original',
description: 'the one and only',
options: 'all the options'
};
});
it('is not marked as custom', function () {
expect(invoke({ def }).isCustom).to.be.false;
});
it('sets a default value', function () {
expect(invoke({ def }).defVal).to.equal(def.value);
});
it('sets a description', function () {
expect(invoke({ def }).description).to.equal(def.description);
});
it('sets options', function () {
expect(invoke({ def }).options).to.equal(def.options);
});
context('that contains a type', function () {
it('sets that type', function () {
def.type = 'something';
expect(invoke({ def }).type).to.equal(def.type);
});
});
context('that contains a value of type array', function () {
it('sets type to array', function () {
def.value = [];
expect(invoke({ def }).type).to.equal('array');
});
});
});
context('when not given a setting definition object', function () {
it('is marked as custom', function () {
expect(invoke().isCustom).to.be.true;
});
it('sets defVal to undefined', function () {
expect(invoke().defVal).to.be.undefined;
});
it('sets description to undefined', function () {
expect(invoke().description).to.be.undefined;
});
it('sets options to undefined', function () {
expect(invoke().options).to.be.undefined;
});
});
});
});
});
function invoke({ def = false, name = 'woah', value = 'forreal' } = {}) {
return toEditableConfig(def, name, value);
}

View file

@ -0,0 +1,17 @@
define(function (require) {
var _ = require('lodash');
var NAMED_EDITORS = ['json', 'array', 'boolean', 'select'];
var NORMAL_EDITOR = ['number', 'string', 'null', 'undefined'];
/**
* @param {object} advanced setting configuration object
* @returns {string} the editor type to use when editing value
*/
function getEditorType(conf) {
if (_.contains(NAMED_EDITORS, conf.type)) return conf.type;
if (_.contains(NORMAL_EDITOR, conf.type)) return 'normal';
}
return getEditorType;
});

View file

@ -0,0 +1,39 @@
define(function (require) {
var _ = require('lodash');
var getValType = require('./get_val_type');
var getEditorType = require('./get_editor_type');
/**
* @param {object} advanced setting definition object
* @param {object} name of setting
* @param {object} current value of setting
* @returns {object} the editable config object
*/
function toEditableConfig(def, name, value) {
var isCustom = !def;
if (isCustom) def = {};
var conf = {
name,
value,
isCustom,
readonly: !!def.readonly,
defVal: def.value,
type: getValType(def, value),
description: def.description,
options: def.options
};
var editor = getEditorType(conf);
conf.json = editor === 'json';
conf.select = editor === 'select';
conf.bool = editor === 'boolean';
conf.array = editor === 'array';
conf.normal = editor === 'normal';
conf.tooComplex = !editor;
return conf;
}
return toEditableConfig;
});

View file

@ -2,7 +2,7 @@
<kbn-settings-objects class="container">
<div class="header">
<h2 class="title">Edit Saved Objects</h2>
<button class="btn btn-default controls" ng-click="exportAll()"><i aria-hidden="true" class="fa fa-download"></i> Export</button>
<button class="btn btn-default controls" ng-click="exportAll()"><i aria-hidden="true" class="fa fa-download"></i> Export Everything</button>
<button file-upload="importAll(fileContents)" class="btn btn-default controls" ng-click><i aria-hidden="true" class="fa fa-upload"></i> Import</button>
</div>
<p>

View file

@ -4,6 +4,7 @@ define(function (require) {
var saveAs = require('@spalger/filesaver').saveAs;
var registry = require('plugins/kibana/settings/saved_object_registry');
var objectIndexHTML = require('plugins/kibana/settings/sections/objects/_objects.html');
const MAX_SIZE = Math.pow(2, 31) - 1;
require('ui/directives/file_upload');
@ -91,11 +92,12 @@ define(function (require) {
retrieveAndExportDocs(objs);
};
$scope.exportAll = function () {
var objs = $scope.services.map(function (service) {
return service.data.map(_.partialRight(_.extend, {type: service.type}));
});
retrieveAndExportDocs(_.flattenDeep(objs));
$scope.exportAll = () => {
Promise.map($scope.services, (service) =>
service.service.find('', MAX_SIZE).then((results) =>
results.hits.map((hit) => _.extend(hit, {type: service.type}))
)
).then((results) => retrieveAndExportDocs(_.flattenDeep(results)));
};
function retrieveAndExportDocs(objs) {

View file

@ -24,20 +24,19 @@ kbn-settings-objects {
margin-bottom: @line-height-computed;
}
.list-unstyled {
// border-top: 1px solid @gray-lighter;
// margin-top: @line-height-computed;
li {
border-bottom: 1px solid @gray-lighter;
border-bottom: 1px solid;
border-bottom-color: @settings-objects-list-border;
padding: 8px;
}
}
.empty {
color: @gray-light;
color: @settings-objects-empty-color;
}
.action-bar {
margin-top: 10px;
background-color: @gray-lighter;
background-color: @settings-objects-action-bar-bg;
padding: 4px 12px;
label {
@ -98,7 +97,7 @@ kbn-settings-objects-view {
width: 100%;
tr.default td.value {
color: @input-color-placeholder;
color: @settings-advanced-table-default-color;
}
td {
@ -127,7 +126,7 @@ kbn-settings-objects-view {
.indices-settings {
i.active {
color: @btn-success-bg;
color: @settings-indices-active-color;
}
tr.field-settings {

View file

@ -126,7 +126,9 @@
<div class="vis-editor-content">
<vis-editor-sidebar class="vis-editor-sidebar" ng-if="chrome.getVisible()"></vis-editor-sidebar>
<div class="collapsible-sidebar">
<vis-editor-sidebar class="vis-editor-sidebar" ng-if="chrome.getVisible()"></vis-editor-sidebar>
</div>
<div class="vis-editor-canvas" ng-class="{ embedded: !chrome.getVisible() }">
<div class="visualize-info" ng-if="savedVis.id">

View file

@ -5,7 +5,7 @@ define(function (require) {
require('plugins/kibana/visualize/editor/agg_filter');
require('ui/visualize');
require('ui/clipboard');
require('ui/collapsible_sidebar');
require('ui/routes')
.when('/visualize/create', {

View file

@ -4,7 +4,6 @@
<div class="form-group">
<label>
Embed this visualization.
<kbn-clipboard copy="conf.shareData().embed"></kbn-clipboard>
<small>Add to your html source. Note all clients must still be able to access kibana</small>
</label>
<div class="form-control" disabled>{{conf.shareData().embed}}</div>
@ -15,7 +14,6 @@
<div class="form-group">
<label>
Share a link
<kbn-clipboard copy="conf.shareData().link"></kbn-clipboard>
</label>
<div class="form-control" disabled>{{conf.shareData().link}}</div>
</div>

View file

@ -10,12 +10,12 @@
.navbar-default .navbar-nav {
&> .active > a:before {
border: 7px solid transparent;
border-bottom-color: @gray-lighter;
border-bottom-color: @vis-editor-navbar-current-tab-color;
}
.danger {
color: @btn-danger-color;
background-color: @btn-danger-bg;
color: @vis-editor-navbar-error-state-color;
background-color: @vis-editor-navbar-error-state-bg;
}
}
@ -35,8 +35,8 @@
right: 0px;
bottom: 0px;
z-index: 10;
background: fadeout(@navbar-default-bg, 10%);
color: white;
background: fadeout(@vis-editor-navbar-modal-bg, 10%);
color: @vis-editor-navbar-modal-color;
text-align: center;
padding-top: 6px;
@ -50,7 +50,7 @@
}
a {
color: white;
color: @vis-editor-navbar-modal-link-color;
}
}
}
@ -64,17 +64,18 @@
}
}
&-sidebar {
.collapsible-sidebar {
.flex-parent(0, 0, auto);
}
// overflow: auto;
&-sidebar {
.flex-parent(1, 0, auto);
// overrided for tablet and desktop
@media (min-width: @screen-md-min) {
flex-basis: @vis-editor-sidebar-basis;
min-width: @vis-editor-sidebar-min-width;
max-width: @vis-editor-sidebar-min-width;
// margin-bottom: (@input-height-base * 2) - 3;
}
nav {
@ -87,8 +88,7 @@
.sidebar-container {
.flex-parent(1, 1, auto);
background-color: @body-bg;
border-right-color: @sidebar-bg;
background-color: @vis-editor-sidebar-bg;
form {
.flex-parent(1, 1, auto);
@ -117,14 +117,14 @@
font-size: 20px;
font-weight: bold;
border: inherit !important;
background-color: @gray-lighter;
background-color: @vis-editor-sidebar-title-bg;
margin-bottom: @vis-editor-agg-editor-spacing;
padding: 2px 5px !important;
}
.sidebar-item-title:hover {
color: @text-color !important;
background-color: @gray-lighter !important;
color: @vis-editor-sidebar-title-hover-color !important;
background-color: @vis-editor-sidebar-title-hover-bg !important;
}
.hintbox {
@ -158,7 +158,7 @@
> span {
width: @vis-editor-nesting-width;
background-color: @brand-success;
background-color: @vis-editor-nesting-indicator-bg;
}
}
@ -177,7 +177,7 @@
&-group {
.flex-parent(0, 1, auto);
color: @text-color;
color: @vis-editor-agg-group-color;
}
&-header {
@ -221,8 +221,8 @@
margin: @vis-editor-agg-editor-spacing 0;
padding: @vis-editor-agg-editor-spacing;
text-align: center;
background: @btn-danger-bg;
color: @btn-danger-color;
background: @vis-editor-agg-error-bg;
color: @vis-editor-agg-error-color;
}
&-editor {
@ -241,7 +241,7 @@
}
a {
color: @link-color;
color: @vis-editor-agg-editor-flags-color;
}
}
@ -275,23 +275,26 @@
&-wide-btn {
border-radius: 0;
border-top: 2px solid @gray-lighter;
border-top: 2px solid;
border-top-color: @vis-editor-agg-wide-btn-border;
&-add {
width: 140px;
margin: -2px auto 5px auto;
text-align: center;
border: 2px solid @gray-lighter;
border: 2px solid;
border-color: @vis-editor-agg-wide-btn-border;
border-top: 0px;
padding: 3px;
border-bottom-right-radius: @border-radius-base;
border-bottom-left-radius: @border-radius-base;
background-color: @body-bg;
background-color: @vis-editor-agg-wide-btn-bg;
font-weight: bold;
}
&-add:hover {
background-color: @gray-lighter;
color: @vis-editor-agg-wide-btn-hover-color;
background-color: @vis-editor-agg-wide-btn-hover-bg;
}
}
@ -310,8 +313,10 @@
}
&-order-agg {
border: 2px solid @gray-lighter;
border: 2px solid;
border-color: @vis-editor-agg-editor-order-border;
border-radius: @border-radius-base;
background-color: @vis-editor-agg-editor-order-bg;
margin: @vis-editor-agg-editor-spacing;
padding: @vis-editor-agg-editor-spacing;
}
@ -326,6 +331,7 @@
display: flex;
flex-direction: column;
overflow: auto;
padding-left: @collapser-width;
&.embedded {
flex-shrink: 1;

View file

@ -41,7 +41,7 @@ define(function (require) {
});
};
this.find = function (searchString) {
this.find = function (searchString, size = 100) {
var self = this;
var body;
if (searchString) {
@ -62,7 +62,7 @@ define(function (require) {
index: kbnIndex,
type: 'visualization',
body: body,
size: 100,
size: size
})
.then(function (resp) {
return {

View file

@ -38,7 +38,7 @@
&-description {
flex: 1 1 auto;
color: @brand-primary;
color: @wizard-vis-type-description-color;
}
}
@ -46,7 +46,7 @@
align-self: flex-end;
&-tab {
background-color: @well-bg;
background-color: @visualize-info-bg;
padding: 5px 10px;
margin-left: @padding-base-horizontal;
border-bottom-left-radius: @border-radius-base;

View file

@ -5,6 +5,7 @@ module.exports = function (kibana) {
title: 'Server Status',
main: 'plugins/statusPage/statusPage',
hidden: true,
url: '/status',
autoload: [].concat(
kibana.autoload.styles,
@ -15,4 +16,3 @@ module.exports = function (kibana) {
}
});
};

View file

@ -5,7 +5,7 @@ module.exports = function ({env, bundle}) {
.join('\n');
let requires = bundle.modules
.map(m => `require('${m}');`)
.map(m => `require(${JSON.stringify(m)});`)
.join('\n');
return `
@ -21,6 +21,8 @@ ${pluginSlug}
*/
window.__KBN__ = {
version: '1.2.3',
buildNum: 1234,
vars: {
kbnIndex: '.kibana',
esShardTimeout: 1500,

View file

@ -87,6 +87,7 @@ module.exports = Joi.object({
lazyHost: Joi.string().hostname().default('localhost'),
lazyPrebuild: Joi.boolean().default(false),
lazyProxyTimeout: Joi.number().default(5 * 60000),
useBundleCache: Joi.boolean().default(Joi.ref('$prod')),
unsafeCache: Joi
.alternatives()
.try(

View file

@ -7,5 +7,5 @@ module.exports = _.once(function (kbnServer) {
// redirect to the single app
let apps = kbnServer.uiExports.apps.toArray();
return apps.length === 1 ? `/app/${apps[0].id}` : '/apps';
return '/app/kibana';
});

View file

@ -1,7 +1,7 @@
let Stream = require('stream');
let moment = require('moment');
let _ = require('lodash');
let numeral = require('numeral');
let numeral = require('@spalger/numeral');
let ansicolors = require('ansicolors');
let stringify = require('json-stringify-safe');
let querystring = require('querystring');

View file

@ -19,6 +19,7 @@ class UiApp {
this.hidden = this.spec.hidden;
this.autoloadOverrides = this.spec.autoload;
this.templateName = this.spec.templateName || 'uiApp';
this.url = this.spec.url || '/app/' + this.id;
// once this resolves, no reason to run it again
this.getModules = _.once(this.getModules);
@ -39,7 +40,7 @@ class UiApp {
}
toJSON() {
return _.pick(this, ['id', 'title', 'description', 'icon', 'main']);
return _.pick(this, ['id', 'title', 'description', 'icon', 'main', 'url']);
}
}

View file

@ -40,33 +40,13 @@ module.exports = async (kbnServer, server, config) => {
server.setupViews(resolve(__dirname, 'views'));
server.exposeStaticFile('/loading.gif', resolve(__dirname, 'public/loading.gif'));
// serve the app switcher
server.route({
path: '/apps',
method: 'GET',
handler: function (req, reply) {
let switcher = uiExports.getHiddenApp('appSwitcher');
if (!switcher) return reply(Boom.notFound('app switcher not installed'));
return reply.renderApp(switcher);
}
});
// serve the app switcher
server.route({
path: '/api/apps',
method: 'GET',
handler: function (req, reply) {
return reply(uiExports.apps);
}
});
server.route({
path: '/app/{id}',
method: 'GET',
handler: function (req, reply) {
let id = req.params.id;
let app = uiExports.apps.byId[id];
if (!app) return reply(Boom.notFound('Unkown app ' + id));
if (!app) return reply(Boom.notFound('Unknown app ' + id));
if (kbnServer.status.isGreen()) {
return reply.renderApp(app);
@ -79,7 +59,7 @@ module.exports = async (kbnServer, server, config) => {
server.decorate('reply', 'renderApp', function (app) {
let payload = {
app: app,
appCount: uiExports.apps.size,
nav: uiExports.apps,
version: kbnServer.version,
buildNum: config.get('pkg.buildNum'),
buildSha: config.get('pkg.buildSha'),

View file

@ -16,7 +16,7 @@ define(function () {
}
/**
* Returns an array of the aggConfigResult and parents up te branch
* Returns an array of the aggConfigResult and parents up the branch
* @returns {array} Array of aggConfigResults
*/
AggConfigResult.prototype.getPath = function () {

View file

@ -0,0 +1,17 @@
describe('ui/metadata', () => {
const expect = require('expect.js');
const metadata = require('ui/metadata');
it('is same data as window.__KBN__', () => {
expect(metadata.version).to.equal(window.__KBN__.version);
expect(metadata.vars.kbnIndex).to.equal(window.__KBN__.vars.kbnIndex);
});
it('is immutable', () => {
expect(() => metadata.foo = 'something').to.throw;
expect(() => metadata.version = 'something').to.throw;
expect(() => metadata.vars = {}).to.throw;
expect(() => metadata.vars.kbnIndex = 'something').to.throw;
});
});

View file

@ -14,61 +14,79 @@ describe('getPoint', function () {
getPoint = Private(require('ui/agg_response/point_series/_get_point'));
}));
it('properly unwraps and scales values without a series', function () {
var row = [ { value: 1 }, { value: 2 }, { value: 3 } ];
var xAspect = { i: 0 };
var seriesAspect = null;
var yScale = 5;
var yAspect = { i: 1 };
var zAspect = { i: 2 };
var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect, zAspect);
describe('Without series aspect', function () {
var seriesAspect;
var xAspect;
var yAspect;
var yScale;
expect(point)
.to.have.property('x', 1)
.and.have.property('y', 10)
.and.have.property('z', 3)
.and.have.property('aggConfigResult', row[1])
.and.not.have.property('series');
beforeEach(function () {
seriesAspect = null;
xAspect = { i: 0 };
yAspect = { i: 1 };
yScale = 5;
});
it('properly unwraps and scales values', function () {
var row = [ { value: 1 }, { value: 2 }, { value: 3 } ];
var zAspect = { i: 2 };
var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect, zAspect);
expect(point)
.to.have.property('x', 1)
.and.have.property('y', 10)
.and.have.property('z', 3)
.and.have.property('aggConfigResult', row[1])
.and.not.have.property('series');
});
it('ignores points with a y value of NaN', function () {
var row = [ { value: 1 }, { value: 'NaN' }];
var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
expect(point).to.be(void 0);
});
});
it('properly unwraps and scales values with a series', function () {
var row = [ { value: 1 }, { value: 2 }, { value: 3 }];
var xAspect = { i: 0 };
var seriesAspect = { i: 1, agg: identFormatted };
var yScale = null;
var yAspect = { i: 2 };
var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
describe('With series aspect', function () {
var row;
var xAspect;
var yAspect;
var yScale;
expect(point)
.to.have.property('x', 1)
.and.have.property('series', 2)
.and.have.property('y', 3)
.and.have.property('aggConfigResult', row[2]);
beforeEach(function () {
row = [ { value: 1 }, { value: 2 }, { value: 3 }];
xAspect = { i: 0 };
yAspect = { i: 2 };
yScale = null;
});
it('properly unwraps and scales values', function () {
var seriesAspect = { i: 1, agg: identFormatted };
var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
expect(point)
.to.have.property('x', 1)
.and.have.property('series', 2)
.and.have.property('y', 3)
.and.have.property('aggConfigResult', row[2]);
});
it('properly formats series values', function () {
var seriesAspect = { i: 1, agg: truthFormatted };
var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
expect(point)
.to.have.property('x', 1)
.and.have.property('series', true)
.and.have.property('y', 3)
.and.have.property('aggConfigResult', row[2]);
});
it ('adds the aggConfig to the points', function () {
var seriesAspect = { i: 1, agg: truthFormatted};
var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
expect(point).to.have.property('aggConfig', truthFormatted);
});
});
it('properly formats series values', function () {
var row = [ { value: 1 }, { value: 2 }, { value: 3 } ];
var xAspect = { i: 0 };
var seriesAspect = { i: 1, agg: truthFormatted };
var yScale = null;
var yAspect = { i: 2 };
var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
expect(point)
.to.have.property('x', 1)
.and.have.property('series', true)
.and.have.property('y', 3)
.and.have.property('aggConfigResult', row[2]);
});
it('ignores points with a y value of NaN', function () {
var row = [ { value: 1 }, { value: 'NaN' }];
var xAspect = { i: 0 };
var seriesAspect = null;
var yScale = 5;
var yAspect = { i: 1 };
var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
expect(point).to.be(void 0);
});
});

View file

@ -26,6 +26,7 @@ define(function (require) {
}
if (series) {
point.aggConfig = series.agg;
point.series = series.agg.fieldFormatter()(unwrap(row[series.i]));
}

View file

@ -12,7 +12,6 @@ define(function (require) {
var series = _(rows)
.transform(function (series, row) {
if (!multiY) {
var point = partGetPoint(row, aspects.y, aspects.z);
if (point) addToSiri(series, point, point.series);

View file

@ -15,15 +15,11 @@ kbn-agg-table-group {
overflow: auto;
tr:hover td {
background-color: lighten(@gray-lighter, 4%);
background-color: @table-row-hover-bg;
}
.cell-hover:hover {
background-color: @gray-lighter;
}
th i.fa-sort {
color: @gray-light;
background-color: @table-cell-hover-hover-bg;
}
}

View file

@ -1,37 +1,71 @@
var _ = require('lodash');
var storage = window.sessionStorage;
const _ = require('lodash');
const reEsc = require('lodash').escapeRegExp;
const { parse, format } = require('url');
function Tab(spec) {
this.id = spec.id;
this.title = spec.title;
this.active = false;
this.resetWhenActive = !!spec.resetWhenActive;
this.lastUrlStoreKey = spec.trackLastPath ? 'lastUrl:' + this.id : null;
this.rootUrl = '/' + this.id;
this.lastUrl = this.lastUrlStoreKey && storage.getItem(this.lastUrlStoreKey);
const urlJoin = (a, b) => {
if (!b) return a;
return `${a}${ a.endsWith('/') ? '' : '/' }${b}`;
};
this.activeIndicatorColor = spec.activeIndicatorColor || null;
if (_.isFunction(this.activeIndicatorColor)) {
// convert to a getter
Object.defineProperty(this, 'activeIndicatorColor', {
get: this.activeIndicatorColor
});
export default class Tab {
constructor(spec = {}) {
this.id = spec.id || '';
this.title = spec.title || '';
this.resetWhenActive = !!spec.resetWhenActive;
this.activeIndicatorColor = spec.activeIndicatorColor || null;
if (_.isFunction(this.activeIndicatorColor)) {
// convert to a getter
Object.defineProperty(this, 'activeIndicatorColor', {
get: this.activeIndicatorColor
});
}
this.active = false;
this.baseUrl = spec.baseUrl || '/';
this.rootUrl = urlJoin(this.baseUrl, this.id);
this.rootRegExp = new RegExp(`^${reEsc(this.rootUrl)}(/|$|\\?|#)`);
this.lastUrlStoreKey = `lastUrl:${this.id}`;
this.lastUrlStore = spec.lastUrlStore;
this.lastUrl = this.lastUrlStore ? this.lastUrlStore.getItem(this.lastUrlStoreKey) : null;
}
href() {
if (this.active) {
return this.resetWhenActive ? this.rootUrl : null;
}
return this.lastUrl || this.rootUrl;
}
updateLastUrlGlobalState(globalState) {
let lastPath = this.getLastPath();
let { pathname, query, hash } = parse(lastPath, true);
query = query || {};
if (!globalState) delete query._g;
else query._g = globalState;
this.setLastUrl(`${this.rootUrl}${format({ pathname, query, hash })}`);
}
getLastPath() {
let { id, rootUrl } = this;
let lastUrl = this.getLastUrl();
if (!lastUrl.startsWith(rootUrl)) {
throw new Error(`Tab "${id}" has invalid root "${rootUrl}" for last url "${lastUrl}"`);
}
return lastUrl.slice(rootUrl.length);
}
setLastUrl(url) {
this.lastUrl = url;
if (this.lastUrlStore) this.lastUrlStore.setItem(this.lastUrlStoreKey, this.lastUrl);
}
getLastUrl() {
return this.lastUrl || this.rootUrl;
}
}
Tab.prototype.persistLastUrl = function (url) {
if (!this.lastUrlStoreKey) return;
this.lastUrl = url;
storage.setItem(this.lastUrlStoreKey, this.lastUrl);
};
Tab.prototype.href = function () {
if (this.active) {
if (this.resetWhenActive) return '#' + this.rootUrl;
return null;
}
return '#' + (this.lastUrl || this.rootUrl);
};
module.exports = Tab;

View file

@ -1,20 +1,12 @@
var _ = require('lodash');
var { startsWith, get, set, omit, wrap, pick } = require('lodash');
var Tab = require('ui/chrome/Tab');
var format = require('url').format;
var parse = _.wrap(require('url').parse, function (parse, path) {
var parsed = parse(path, true);
return {
pathname: parsed.pathname,
query: parsed.query || {},
hash: parsed.hash
};
});
function TabCollection() {
var { parse } = require('url');
function TabCollection(opts = {}) {
var tabs = [];
var specs = null;
var defaults = null;
var defaults = opts.defaults || {};
var activeTab = null;
this.set = function (_specs) {
@ -23,7 +15,7 @@ function TabCollection() {
};
this.setDefaults = function () {
defaults = _.clone(arguments[0]);
defaults = _.defaults({}, arguments[0], defaults);
this._rebuildTabs();
};
@ -42,20 +34,19 @@ function TabCollection() {
return activeTab;
};
this.consumeRouteUpdate = function ($location, persist) {
var url = parse($location.url(), true);
var id = $location.path().split('/')[1] || '';
this.consumeRouteUpdate = function (href, persist) {
tabs.forEach(function (tab) {
var active = tab.active = (tab.id === id);
var lastUrl = active ? url : parse(tab.lastUrl || tab.rootUrl);
lastUrl.query._g = url.query._g;
if (tab.active) activeTab = tab;
if (persist) {
tab.persistLastUrl(format(lastUrl));
tab.active = tab.rootRegExp.test(href);
if (tab.active) {
activeTab = tab;
activeTab.setLastUrl(href);
}
});
if (!persist || !activeTab) return;
let globalState = get(parse(activeTab.getLastPath(), true), 'query._g');
tabs.forEach(tab => tab.updateLastUrlGlobalState(globalState));
};
}

View file

@ -0,0 +1,241 @@
const Tab = require('../Tab');
const expect = require('expect.js');
const TabFakeStore = require('./_TabFakeStore');
describe('Chrome Tab', function () {
describe('construction', function () {
it('accepts id, title, resetWhenActive, trackLastUrl, activeIndicatorColor, baseUrl', function () {
const tab = new Tab({
id: 'foo',
title: 'Foo App',
resetWhenActive: false,
activeIndicatorColor: true,
baseUrl: 'proto:host.domain:999'
});
expect(tab.id).to.equal('foo');
expect(tab.title).to.equal('Foo App');
expect(tab.resetWhenActive).to.equal(false);
expect(tab.activeIndicatorColor).to.equal(true);
expect(tab.rootUrl).to.equal('proto:host.domain:999/foo');
const tab2 = new Tab({
id: 'bar',
title: 'Bar App',
resetWhenActive: true,
activeIndicatorColor: false,
baseUrl: 'proto:host.domain:999/sub/#/'
});
expect(tab2.id).to.equal('bar');
expect(tab2.title).to.equal('Bar App');
expect(tab2.resetWhenActive).to.equal(true);
expect(tab2.activeIndicatorColor).to.equal(null);
expect(tab2.rootUrl).to.equal('proto:host.domain:999/sub/#/bar');
});
it('starts inactive', function () {
const tab = new Tab();
expect(tab.active).to.equal(false);
});
it('uses the id to set the rootUrl', function () {
const id = 'foo';
const tab = new Tab({ id });
expect(tab.id).to.equal(id);
expect(tab.rootUrl).to.equal(`/${id}`);
});
it('creates a regexp for matching the rootUrl', function () {
const tab = new Tab({ id: 'foo' });
expect('/foo').to.match(tab.rootRegExp);
expect('/foo/bar').to.match(tab.rootRegExp);
expect('/foo/bar/max').to.match(tab.rootRegExp);
expect('/foo?bar=baz').to.match(tab.rootRegExp);
expect('/foo/?bar=baz').to.match(tab.rootRegExp);
expect('/foo#?bar=baz').to.match(tab.rootRegExp);
expect('/foobar').to.not.match(tab.rootRegExp);
expect('site.com/foo#?bar=baz').to.not.match(tab.rootRegExp);
expect('http://site.com/foo#?bar=baz').to.not.match(tab.rootRegExp);
});
it('includes the baseUrl in the rootRegExp if specified', function () {
const tab = new Tab({
id: 'foo',
baseUrl: 'http://spiderman.com/kibana'
});
expect('http://spiderman.com/kibana/foo/bar').to.match(tab.rootRegExp);
expect('/foo').to.not.match(tab.rootRegExp);
expect('https://spiderman.com/kibana/foo/bar').to.not.match(tab.rootRegExp);
});
it('accepts a function for activeIndicatorColor', function () {
let i = 0;
const tab = new Tab({
activeIndicatorColor: function () {
return i++;
}
});
expect(tab.activeIndicatorColor).to.equal(0);
expect(tab.activeIndicatorColor).to.equal(1);
expect(tab.activeIndicatorColor).to.equal(2);
expect(tab.activeIndicatorColor).to.equal(3);
});
it('discovers the lastUrl', function () {
const lastUrlStore = new TabFakeStore();
const tab = new Tab({ id: 'foo', lastUrlStore });
expect(tab.lastUrl).to.not.equal('bar');
tab.setLastUrl('bar');
expect(tab.lastUrl).to.equal('bar');
const tab2 = new Tab({ id: 'foo', lastUrlStore });
expect(tab2.lastUrl).to.equal('bar');
});
});
describe('#setLastUrl()', function () {
it('updates the lastUrl and storage value if passed a lastUrlStore', function () {
const lastUrlStore = new TabFakeStore();
const tab = new Tab({ id: 'foo', lastUrlStore });
expect(tab.lastUrl).to.not.equal('foo');
tab.setLastUrl('foo');
expect(tab.lastUrl).to.equal('foo');
expect(lastUrlStore.getItem(tab.lastUrlStoreKey)).to.equal('foo');
});
it('only updates lastUrl if no lastUrlStore', function () {
const tab = new Tab({ id: 'foo' });
expect(tab.lastUrl).to.equal(null);
tab.setLastUrl('foo');
expect(tab.lastUrl).to.equal('foo');
const tab2 = new Tab({ id: 'foo' });
expect(tab2.lastUrl).to.not.equal('foo');
});
});
describe('#href()', function () {
it('returns the rootUrl/id be default', function () {
const tab = new Tab({ id: 'foo' });
expect(tab.href()).to.equal(tab.rootUrl);
});
it('returns the lastUrl if tracking is on', function () {
const tab = new Tab({ id: 'foo' });
tab.setLastUrl('okay');
expect(tab.href()).to.equal('okay');
});
describe('when the tab is active', function () {
it('returns the rootUrl when resetWhenActive: true', function () {
const id = 'foo';
const resetWhenActive = true;
const tab = new Tab({ id, resetWhenActive });
tab.active = true;
expect(tab.href()).to.not.equal('butt');
expect(tab.href()).to.equal(tab.rootUrl);
});
it('or returns null when not', function () {
const tab = new Tab({ id: 'foo', resetWhenActive: false });
tab.active = true;
expect(tab.href()).to.not.equal('butt');
expect(tab.href()).to.equal(null);
});
});
});
describe('#getLastPath()', function () {
it('parses a path out of the lastUrl by removing the baseUrl', function () {
const baseUrl = 'http://local:5601/app/visualize#';
const tab = new Tab({ baseUrl });
tab.setLastUrl('http://local:5601/app/visualize#/index');
expect(tab.getLastPath()).to.equal('/index');
});
it('throws an error if the lastUrl does not extend the root url', function () {
expect(function () {
const baseUrl = 'http://local:5601/app/visualize#';
const tab = new Tab({ baseUrl });
tab.setLastUrl('http://local:5601/');
tab.getLastPath();
}).to.throwError(/invalid.*root/);
});
});
describe('updateLastUrlGlobalState', function () {
const bases = [
'http://local:5601',
'',
'weird.domain/with/subpath?path#',
'weird.domain/with/#hashpath',
];
context('with new state sets _g properly', function () {
const paths = [
[ '/', '/?_g=newState' ],
[ '/?first', '/?first=&_g=newState' ],
[ '/path?first=1&_g=afterHash', '/path?first=1&_g=newState' ],
[ '/?first=1&_g=second', '/?first=1&_g=newState' ],
[ '/?g=first', '/?g=first&_g=newState' ],
[ '/a?first=1&_g=second', '/a?first=1&_g=newState' ],
[ '/?first=1&_g=second', '/?first=1&_g=newState' ],
[ '/?first&g=second', '/?first=&g=second&_g=newState' ],
];
bases.forEach(baseUrl => {
paths.forEach(([pathFrom, pathTo]) => {
const fromUrl = `${baseUrl}${pathFrom}`;
const toUrl = `${baseUrl}${pathTo}`;
it(`${fromUrl} => ${toUrl}`, function () {
const tab = new Tab({ baseUrl });
tab.setLastUrl(fromUrl);
tab.updateLastUrlGlobalState('newState');
expect(tab.getLastUrl()).to.equal(toUrl);
});
});
});
});
context('with new empty state removes _g', function () {
const paths = [
[ '/', '/' ],
[ '/?first', '/?first=' ],
[ '/path?first=1&_g=afterHash', '/path?first=1' ],
[ '/?first=1&_g=second', '/?first=1' ],
[ '/?g=first', '/?g=first' ],
[ '/a?first=1&_g=second', '/a?first=1' ],
[ '/?first=1&_g=second', '/?first=1' ],
[ '/?first&g=second', '/?first=&g=second' ],
];
bases.forEach(baseUrl => {
paths.forEach(([pathFrom, pathTo]) => {
const fromUrl = `${baseUrl}${pathFrom}`;
const toUrl = `${baseUrl}${pathTo}`;
it(`${fromUrl}`, function () {
const tab = new Tab({ baseUrl });
tab.setLastUrl(fromUrl);
tab.updateLastUrlGlobalState();
expect(tab.getLastUrl()).to.equal(toUrl);
});
});
});
});
});
});

View file

@ -0,0 +1,73 @@
const expect = require('expect.js');
const { indexBy, random } = require('lodash');
const TabFakeStore = require('./_TabFakeStore');
const TabCollection = require('../TabCollection');
const Tab = require('../Tab');
describe('Chrome TabCollection', function () {
describe('empty state', function () {
it('has no tabs', function () {
const tabs = new TabCollection();
expect(tabs.get()).to.eql([]);
});
it('has no active tab', function () {
const tabs = new TabCollection();
expect(!tabs.getActive()).to.equal(true);
});
});
describe('#set()', function () {
it('consumes an ordered list of Tab specs', function () {
const tabs = new TabCollection();
tabs.set([
{ id: 'foo' },
{ id: 'bar' }
]);
const ts = tabs.get();
expect(ts.length).to.equal(2);
expect(ts[0].id).to.equal('foo');
expect(ts[1].id).to.equal('bar');
});
});
describe('#setDefaults()', function () {
it('applies the defaults used to create tabs', function () {
const tabs = new TabCollection();
tabs.setDefaults({ id: 'thing' });
tabs.set([ {} ]);
expect(tabs.get()[0].id).to.equal('thing');
});
it('recreates existing tabs with new defaults', function () {
const tabs = new TabCollection();
tabs.set([ {} ]);
expect(!tabs.get()[0].id).to.equal(true);
tabs.setDefaults({ id: 'thing' });
expect(tabs.get()[0].id).to.equal('thing');
});
});
describe('#consumeRouteUpdate()', function () {
it('updates the active tab', function () {
const store = new TabFakeStore();
const baseUrl = `http://localhost:${random(1000, 9999)}`;
const tabs = new TabCollection({ store, defaults: { baseUrl } });
tabs.set([
{ id: 'a' },
{ id: 'b' }
]);
tabs.consumeRouteUpdate(`${baseUrl}/a`);
const {a, b} = indexBy(tabs.get(), 'id');
expect(a.active).to.equal(true);
expect(b.active).to.equal(false);
expect(tabs.getActive()).to.equal(a);
});
});
});

View file

@ -0,0 +1,9 @@
const store = Symbol('store');
export default class TabFakeStore {
constructor() { this[store] = new Map(); }
getItem(k) { return this[store].get(k); }
setItem(k, v) { return this[store].set(k, v); }
getKeys() { return [ ...this[store].keys() ]; }
getValues() { return [ ...this[store].values() ]; }
}

View file

@ -0,0 +1,163 @@
const expect = require('expect.js');
const setup = require('../apps');
const TabFakeStore = require('../../__tests__/_TabFakeStore');
describe('Chrome API :: apps', function () {
describe('#get/setShowAppsLink()', function () {
describe('defaults to false if there are less than two apps', function () {
it('appCount = 0', function () {
const chrome = {};
setup(chrome, { nav: [] });
expect(chrome.getShowAppsLink()).to.equal(false);
});
it('appCount = 1', function () {
const chrome = {};
setup(chrome, { nav: [ { url: '/' } ] });
expect(chrome.getShowAppsLink()).to.equal(false);
});
});
describe('defaults to true if there are two or more apps', function () {
it('appCount = 2', function () {
const chrome = {};
setup(chrome, { nav: [ { url: '/' }, { url: '/2' } ] });
expect(chrome.getShowAppsLink()).to.equal(true);
});
it('appCount = 3', function () {
const chrome = {};
setup(chrome, { nav: [ { url: '/' }, { url: '/2' }, { url: '/3' } ] });
expect(chrome.getShowAppsLink()).to.equal(true);
});
});
it('is chainable', function () {
const chrome = {};
setup(chrome, { nav: [ { url: '/' } ] });
expect(chrome.setShowAppsLink(true)).to.equal(chrome);
});
it('can be changed', function () {
const chrome = {};
setup(chrome, { nav: [ { url: '/' } ] });
expect(chrome.setShowAppsLink(true).getShowAppsLink()).to.equal(true);
expect(chrome.getShowAppsLink()).to.equal(true);
expect(chrome.setShowAppsLink(false).getShowAppsLink()).to.equal(false);
expect(chrome.getShowAppsLink()).to.equal(false);
});
});
describe('#getApp()', function () {
it('returns a clone of the current app', function () {
const chrome = {};
const app = { url: '/' };
setup(chrome, { app });
expect(chrome.getApp()).to.eql(app);
expect(chrome.getApp()).to.not.equal(app);
});
it('returns undefined if no active app', function () {
const chrome = {};
setup(chrome, {});
expect(chrome.getApp()).to.equal(undefined);
});
});
describe('#getAppTitle()', function () {
it('returns the title property of the current app', function () {
const chrome = {};
const app = { url: '/', title: 'foo' };
setup(chrome, { app });
expect(chrome.getAppTitle()).to.eql('foo');
});
it('returns undefined if no active app', function () {
const chrome = {};
setup(chrome, {});
expect(chrome.getAppTitle()).to.equal(undefined);
});
});
describe('#getAppUrl()', function () {
it('returns the resolved url of the current app', function () {
const chrome = {};
const app = { url: '/foo' };
setup(chrome, { app });
const a = document.createElement('a');
a.setAttribute('href', app.url);
expect(chrome.getAppUrl()).to.equal(a.href);
});
it('returns undefined if no active app', function () {
const chrome = {};
setup(chrome, {});
expect(chrome.getAppUrl()).to.equal(undefined);
});
});
describe('#getInjected()', function () {
describe('called without args', function () {
it('returns a clone of all injectedVars', function () {
const chrome = {};
const vars = { name: 'foo' };
setup(chrome, { vars });
expect(chrome.getInjected()).to.eql(vars);
expect(chrome.getInjected()).to.not.equal(vars);
});
});
describe('called with a var name', function () {
it('returns the var at that name', function () {
const chrome = {};
const vars = { name: 'foo' };
setup(chrome, { vars });
expect(chrome.getInjected('name')).to.equal('foo');
});
});
describe('called with a var name and default', function () {
it('returns the default when the var is undefined', function () {
const chrome = {};
const vars = { name: undefined };
setup(chrome, { vars });
expect(chrome.getInjected('name', 'bar')).to.equal('bar');
});
it('returns null when the var is null', function () {
const chrome = {};
const vars = { name: null };
setup(chrome, { vars });
expect(chrome.getInjected('name', 'bar')).to.equal(null);
});
it('returns var if not undefined', function () {
const chrome = {};
const vars = { name: 'kim' };
setup(chrome, { vars });
expect(chrome.getInjected('name', 'bar')).to.equal('kim');
});
});
describe('#get/setLastUrlFor()', function () {
it('reads/writes last url from storage', function () {
const chrome = {};
const store = new TabFakeStore();
setup(chrome, { appUrlStore: store });
expect(chrome.getLastUrlFor('app')).to.equal(undefined);
chrome.setLastUrlFor('app', 'url');
expect(chrome.getLastUrlFor('app')).to.equal('url');
expect(store.getKeys().length).to.equal(1);
expect(store.getValues().shift()).to.equal('url');
});
});
});
});

View file

@ -1,7 +1,11 @@
var modules = require('ui/modules');
var $ = require('jquery');
var _ = require('lodash');
require('../appSwitcher');
var modules = require('ui/modules');
var ConfigTemplate = require('ui/ConfigTemplate');
require('ui/directives/config');
module.exports = function (chrome, internals) {
chrome.setupAngular = function () {
var kibana = modules.get('kibana');
@ -44,7 +48,13 @@ module.exports = function (chrome, internals) {
chrome.setVisible(!Boolean($location.search().embed));
// listen for route changes, propogate to tabs
var onRouteChange = _.bindKey(internals.tabs, 'consumeRouteUpdate', $location, chrome.getVisible());
var onRouteChange = function () {
let { href } = window.location;
let persist = chrome.getVisible();
internals.trackPossibleSubUrl(href);
internals.tabs.consumeRouteUpdate(href, persist);
};
$rootScope.$on('$routeChangeSuccess', onRouteChange);
$rootScope.$on('$routeUpdate', onRouteChange);
onRouteChange();
@ -52,6 +62,9 @@ module.exports = function (chrome, internals) {
// and some local values
$scope.httpActive = $http.pendingRequests;
$scope.notifList = require('ui/notify')._notifs;
$scope.appSwitcherTemplate = new ConfigTemplate({
switcher: '<app-switcher></app-switcher>'
});
return chrome;
}

View file

@ -1,7 +1,14 @@
var _ = require('lodash');
const { clone, get } = require('lodash');
const { resolve } = require('url');
module.exports = function (chrome, internals) {
if (internals.app) {
internals.app.url = resolve(window.location.href, internals.app.url);
}
internals.appUrlStore = internals.appUrlStore || window.sessionStorage;
/**
* ui/chrome apps API
*
@ -16,24 +23,33 @@ module.exports = function (chrome, internals) {
};
chrome.getShowAppsLink = function () {
return internals.showAppsLink == null ? internals.appCount > 1 : internals.showAppsLink;
return internals.showAppsLink == null ? internals.nav.length > 1 : internals.showAppsLink;
};
chrome.getApp = function () {
return _.clone(internals.app);
return clone(internals.app);
};
chrome.getAppTitle = function () {
return internals.app.title;
return get(internals, ['app', 'title']);
};
chrome.getAppId = function () {
return internals.app.id;
chrome.getAppUrl = function () {
return get(internals, ['app', 'url']);
};
chrome.getInjected = function (name, def) {
if (name == null) return _.clone(internals.vars) || {};
return _.get(internals.vars, name, def);
if (name == null) return clone(internals.vars) || {};
return get(internals.vars, name, def);
};
chrome.getLastUrlFor = function (appId) {
return internals.appUrlStore.getItem(`appLastUrl:${appId}`);
};
chrome.setLastUrlFor = function (appId, url) {
internals.appUrlStore.setItem(`appLastUrl:${appId}`, url);
};
};

View file

@ -0,0 +1,34 @@
module.exports = function (chrome, internals) {
const { startsWith } = require('lodash');
chrome.getNavLinks = function () {
return internals.nav;
};
chrome.getLastSubUrlFor = function (url) {
return internals.appUrlStore.getItem(`lastSubUrl:${url}`);
};
internals.trackPossibleSubUrl = function (url) {
for (const link of internals.nav) {
if (startsWith(url, link.url)) {
link.lastSubUrl = url;
internals.appUrlStore.setItem(`lastSubUrl:${link.url}`, url);
}
}
};
internals.nav.forEach(link => {
// convert all link urls to absolute urls
var a = document.createElement('a');
a.setAttribute('href', link.url);
link.url = a.href;
link.lastSubUrl = chrome.getLastSubUrlFor(link.url);
if (link.url === chrome.getAppUrl()) {
link.active = true;
}
});
};

View file

@ -1,7 +1,14 @@
var _ = require('lodash');
var TabCollection = require('../TabCollection');
module.exports = function (chrome, internals) {
internals.tabs = new TabCollection({
defaults: {
baseUrl: `${chrome.getAppUrl()}#/`
}
});
/**
* ui/chrome tabs API
*
@ -26,7 +33,7 @@ module.exports = function (chrome, internals) {
* when the the tab is considered active, should clicking it
* cause a redirect to just the id?
*
* trackLastPath {boolean}
* trackLastUrl {boolean}
* When this tab is active, should the current path be tracked
* and persisted to session storage, then used as the tabs href attribute when the user navigates
* away from the tab?

View file

@ -60,4 +60,41 @@ module.exports = function (chrome, internals) {
return internals.brand[item];
};
/**
* Adds a class to the application node
* @param {string} - the class name to add
* @return {chrome}
*/
chrome.addApplicationClass = function (val) {
var classes = internals.applicationClasses || [];
classes.push(val);
classes = _.uniq(classes);
internals.applicationClasses = classes;
return chrome;
};
/**
* Removes a class from the application node. Note: this only
* removes classes that were added via the addApplicationClass method
* @param {string|[string]} - class or classes to be removed
* @return {chrome}
*/
chrome.removeApplicationClass = function (val) {
var classesToRemove = [].concat(val || []);
var classes = internals.applicationClasses || [];
_.pull(classes, ...classesToRemove);
internals.applicationClasses = classes;
return chrome;
};
/**
* @return {string} - a space delimited string of the classes added by the
* addApplicationClass method
*/
chrome.getApplicationClasses = function () {
return internals.applicationClasses.join(' ');
};
};

View file

@ -0,0 +1,221 @@
var sinon = require('auto-release-sinon');
var ngMock = require('ngMock');
var $ = require('jquery');
var expect = require('expect.js');
var constant = require('lodash').constant;
var set = require('lodash').set;
var cloneDeep = require('lodash').cloneDeep;
var indexBy = require('lodash').indexBy;
require('ui/chrome');
require('ui/chrome/appSwitcher');
var DomLocationProvider = require('ui/domLocation');
describe('appSwitcher directive', function () {
var env;
beforeEach(ngMock.module('kibana'));
function setup(href, links) {
return ngMock.inject(function ($window, $rootScope, $compile, Private) {
var domLocation = Private(DomLocationProvider);
$rootScope.chrome = {
getNavLinks: constant(cloneDeep(links)),
};
env = {
$scope: $rootScope,
$el: $compile($('<app-switcher>'))($rootScope),
currentHref: href,
location: domLocation
};
Object.defineProperties(domLocation, {
href: {
get: function () { return env.currentHref; },
set: function (val) { return env.currentHref = val; },
},
reload: {
value: sinon.stub()
}
});
env.$scope.$digest();
});
}
context('when one link is for the active app', function () {
var myLink = {
active: true,
title: 'myLink',
url: 'http://localhost:555/app/myApp',
lastSubUrl: 'http://localhost:555/app/myApp#/lastSubUrl'
};
var notMyLink = {
active: false,
title: 'notMyLink',
url: 'http://localhost:555/app/notMyApp',
lastSubUrl: 'http://localhost:555/app/notMyApp#/lastSubUrl'
};
beforeEach(setup('http://localhost:5555/app/myApp/', [myLink, notMyLink]));
it('links to the inactive apps base url', function () {
var $myLink = env.$el.findTestSubject('appLink').eq(0);
expect($myLink.prop('href')).to.be(myLink.url);
expect($myLink.prop('href')).to.not.be(myLink.lastSubUrl);
});
it('links to the inactive apps last sub url', function () {
var $notMyLink = env.$el.findTestSubject('appLink').eq(1);
expect($notMyLink.prop('href')).to.be(notMyLink.lastSubUrl);
expect($notMyLink.prop('href')).to.not.be(notMyLink.url);
});
});
context('when none of the links are for the active app', function () {
var myLink = {
active: false,
title: 'myLink',
url: 'http://localhost:555/app/myApp',
lastSubUrl: 'http://localhost:555/app/myApp#/lastSubUrl'
};
var notMyLink = {
active: false,
title: 'notMyLink',
url: 'http://localhost:555/app/notMyApp',
lastSubUrl: 'http://localhost:555/app/notMyApp#/lastSubUrl'
};
beforeEach(setup('http://localhost:5555/app/myApp/', [myLink, notMyLink]));
it('links to the lastSubUrl for each', function () {
var $links = env.$el.findTestSubject('appLink');
var $myLink = $links.eq(0);
var $notMyLink = $links.eq(1);
expect($myLink.prop('href')).to.be(myLink.lastSubUrl);
expect($myLink.prop('href')).to.not.be(myLink.url);
expect($notMyLink.prop('href')).to.be(notMyLink.lastSubUrl);
expect($notMyLink.prop('href')).to.not.be(notMyLink.url);
});
});
context('clicking a link with matching href but missing hash', function () {
var url = 'http://localhost:555/app/myApp?query=1';
beforeEach(setup(url + '#/lastSubUrl', [
{ url: url }
]));
it('just prevents propogation (no reload)', function () {
var event = new $.Event('click');
expect(env.location.reload.callCount).to.be(0);
expect(event.isDefaultPrevented()).to.be(false);
expect(event.isPropagationStopped()).to.be(false);
var $link = env.$el.findTestSubject('appLink');
expect($link.prop('href')).to.be(url);
$link.trigger(event);
expect(env.location.reload.callCount).to.be(0);
expect(event.isDefaultPrevented()).to.be(false);
expect(event.isPropagationStopped()).to.be(true);
});
});
context('clicking a link that matches entire url', function () {
var url = 'http://localhost:555/app/myApp#/lastSubUrl';
beforeEach(setup(url, [
{ url: url }
]));
it('calls window.location.reload and prevents propogation', function () {
var event = new $.Event('click');
expect(env.location.reload.callCount).to.be(0);
expect(event.isDefaultPrevented()).to.be(false);
expect(event.isPropagationStopped()).to.be(false);
var $link = env.$el.findTestSubject('appLink');
expect($link.prop('href')).to.be(env.currentHref);
$link.trigger(event);
expect(env.location.reload.callCount).to.be(1);
expect(event.isDefaultPrevented()).to.be(false);
expect(event.isPropagationStopped()).to.be(true);
});
});
context('clicking a link with matching href but changed hash', function () {
var rootUrl = 'http://localhost:555/app/myApp?query=1';
var url = rootUrl + '#/lastSubUrl2';
beforeEach(setup(url + '#/lastSubUrl', [
{ url: url }
]));
it('calls window.location.reload and prevents propogation', function () {
var event = new $.Event('click');
expect(env.location.reload.callCount).to.be(0);
expect(event.isDefaultPrevented()).to.be(false);
expect(event.isPropagationStopped()).to.be(false);
var $link = env.$el.findTestSubject('appLink');
expect($link.prop('href')).to.be(url);
$link.trigger(event);
expect(env.location.reload.callCount).to.be(1);
expect(event.isDefaultPrevented()).to.be(false);
expect(event.isPropagationStopped()).to.be(true);
});
});
context('clicking a link with matching host', function () {
beforeEach(setup('http://localhost:555/someOtherPath', [
{
active: true,
url: 'http://localhost:555/app/myApp'
}
]));
it('allows click through', function () {
var event = new $.Event('click');
expect(env.location.reload.callCount).to.be(0);
expect(event.isPropagationStopped()).to.be(false);
env.$el.findTestSubject('appLink').trigger(event);
expect(env.location.reload.callCount).to.be(0);
expect(event.isPropagationStopped()).to.be(false);
});
});
context('clicking a link with matching host and path', function () {
beforeEach(setup('http://localhost:555/app/myApp?someQuery=true', [
{
active: true,
url: 'http://localhost:555/app/myApp?differentQuery=true'
}
]));
it('allows click through', function () {
var event = new $.Event('click');
expect(env.location.reload.callCount).to.be(0);
expect(event.isPropagationStopped()).to.be(false);
env.$el.findTestSubject('appLink').trigger(event);
expect(env.location.reload.callCount).to.be(0);
expect(event.isPropagationStopped()).to.be(false);
});
});
});

View file

@ -0,0 +1,18 @@
<div class="app-links">
<div
class="app-link"
ng-repeat="link in switcher.getNavLinks() | orderBy:'title'"
ng-class="{ active: link.active }">
<a
ng-click="switcher.ensureNavigation($event, link)"
ng-href="{{ link.active ? link.url : (link.lastSubUrl || link.url) }}"
data-test-subj="appLink">
<div ng-if="link.icon" ng-style="{ 'background-image': 'url(../' + link.icon + ')' }" class="app-icon"></div>
<div ng-if="!link.icon" class="app-icon app-icon-missing">{{ link.title[0] }}</div>
<div class="app-title">{{ link.title }}</div>
</a>
</div>
</div>

View file

@ -0,0 +1,51 @@
var parse = require('url').parse;
var bindKey = require('lodash').bindKey;
require('../appSwitcher/appSwitcher.less');
var DomLocationProvider = require('ui/domLocation');
require('ui/modules')
.get('kibana')
.directive('appSwitcher', function () {
return {
restrict: 'E',
template: require('./appSwitcher.html'),
controllerAs: 'switcher',
controller: function ($scope, Private) {
var domLocation = Private(DomLocationProvider);
// since we render this in an isolate scope we can't "require: ^chrome", but
// rather than remove all helpfull checks we can just check here.
if (!$scope.chrome || !$scope.chrome.getNavLinks) {
throw new TypeError('appSwitcher directive requires the "chrome" config-object');
}
this.getNavLinks = bindKey($scope.chrome, 'getNavLinks');
// links don't cause full-navigation events in certain scenarios
// so we force them when needed
this.ensureNavigation = function (event, app) {
if (event.isDefaultPrevented() || event.altKey || event.metaKey || event.ctrlKey) {
return;
}
var toParsed = parse(event.delegateTarget.href);
var fromParsed = parse(domLocation.href);
var sameProto = toParsed.protocol === fromParsed.protocol;
var sameHost = toParsed.host === fromParsed.host;
var samePath = toParsed.path === fromParsed.path;
if (sameProto && sameHost && samePath) {
toParsed.hash && domLocation.reload();
// event.preventDefault() keeps the browser from seeing the new url as an update
// and even setting window.location does not mimic that behavior, so instead
// we use stopPropagation() to prevent angular from seeing the click and
// starting a digest cycle/attempting to handle it in the router.
event.stopPropagation();
}
};
}
};
});

View file

@ -0,0 +1,58 @@
@import (reference) "~ui/styles/variables";
@app-icon-size: 48px;
@app-icon-padding: 10px;
.app-links {
text-align: justify;
.app-link {
display: inline-block;
vertical-align: top;
text-align: left;
width: @app-icon-size + (@app-icon-padding * 2);
margin: 0px 10px;
padding: @app-icon-padding;
border-radius: @border-radius-base;
.app-icon {
display: block;
height: @app-icon-size;
width: @app-icon-size;
background-position: center;
background-size: contain;
border-radius: @border-radius-base;
background-color: @gray-light;
width: 100%;
&-missing {
text-align: center;
font-size: 2.7em;
font-weight: bold;
font-family: @font-family-sans-serif;
color: #fff;
}
}
.app-title {
color: @text-color;
font-size: 0.9em;
width: 100%;
text-align: center;
margin-top: 3px;
}
&:hover .app-title {
text-decoration: underline;
}
&.active {
background: @gray-lighter;
.app-title {
text-decoration: underline;
}
}
}
}

View file

@ -41,15 +41,17 @@
<li ng-if="chrome.getBrand('title')" class="navbar-brand">{{ chrome.getBrand('title') }}</li>
<li ng-if="chrome.getShowAppsLink()">
<a href="/apps"><i class="fa fa-th" alt="Go to app switcher"></i></a>
</li>
<li ng-repeat="tab in chrome.getTabs()" ng-class="{ active: tab.active }">
<a ng-href="{{ tab.href() }}" ng-style="{ 'border-bottom-color': tab.activeIndicatorColor }">
{{ tab.title }}
</a>
</li>
<li class="to-body" ng-class="{ active: appSwitcherTemplate.is('switcher') }" ng-if="chrome.getShowAppsLink()">
<a ng-click="appSwitcherTemplate.toggle('switcher')">
<i class="fa fa-th" alt="Show app switcher"></i>
</a>
</li>
</ul>
<ul ng-show="timefilter.enabled" class="nav navbar-nav navbar-right navbar-timepicker">
@ -95,6 +97,12 @@
<!-- /Full navbar -->
</nav>
<config
config-template="appSwitcherTemplate"
config-object="chrome"
config-close="appSwitcherTemplate.close">
</config>
<config
ng-show="timefilter.enabled"
config-template="pickerTemplate"
@ -102,5 +110,5 @@
config-close="pickerTemplate.close">
</config>
<div class="application" ng-class="'tab-' + chrome.getActiveTabId('-none-')" ng-view></div>
<div class="application" ng-class="'tab-' + chrome.getActiveTabId('-none-') + ' ' + chrome.getApplicationClasses()" ng-view></div>
</div>

View file

@ -8,22 +8,19 @@ require('ui/timefilter');
require('ui/private');
require('ui/promises');
var metadata = require('ui/metadata');
var TabCollection = require('ui/chrome/TabCollection');
var chrome = {
navBackground: '#222222',
logo: null,
smallLogo: null
};
var internals = _.assign(
_.cloneDeep(window.__KBN__ || {}),
var chrome = {};
var internals = _.defaults(
_.cloneDeep(metadata),
{
tabs: new TabCollection(),
rootController: null,
rootTemplate: null,
showAppsLink: null,
brand: null
brand: null,
nav: [],
applicationClasses: []
}
);
@ -33,6 +30,7 @@ $('<link>').attr({
}).appendTo('head');
require('./api/apps')(chrome, internals);
require('./api/nav')(chrome, internals);
require('./api/angular')(chrome, internals);
require('./api/controls')(chrome, internals);
require('./api/tabs')(chrome, internals);

View file

@ -1,100 +0,0 @@
var angular = require('angular');
var _ = require('lodash');
var sinon = require('auto-release-sinon');
var expect = require('expect.js');
var $ = require('jquery');
var ngMock = require('ngMock');
require('ui/clipboard');
describe('Clipboard directive', function () {
var $scope;
var $rootScope;
var $compile;
var $interpolate;
var el;
var tips;
function init() {
// load the application
ngMock.module('kibana');
ngMock.inject(function (_$rootScope_, _$compile_, _$interpolate_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$interpolate = _$interpolate_;
$rootScope.text = 'foo';
el = $compile('<kbn-clipboard copy="text"></kbn-clipboard>')($rootScope);
$scope = el.scope();
$scope.$digest();
});
}
describe.skip('With flash disabled', function () {
beforeEach(function () {
sinon.stub(window.ZeroClipboard, 'isFlashUnusable', _.constant(true));
init();
});
it('should be an empty element', function () {
expect(el.children()).to.have.length(0);
});
it('should not show the tooltip', function () {
var clip = el.find('[tooltip]');
expect(clip).to.have.length(0);
});
it('should not show the clipboard button', function () {
var clip = el.find('[clip-copy]');
expect(clip).to.have.length(0);
});
});
describe.skip('With flash enabled', function () {
beforeEach(function () {
sinon.stub(window.ZeroClipboard, 'isFlashUnusable', _.constant(false));
init();
});
it('should contain an element with clip-copy', function () {
var clip = el.find('[clip-copy]');
expect(clip).to.have.length(1);
});
it('should have a tooltip', function () {
var clip = el.find('[tooltip]');
expect(clip).to.have.length(1);
var clipText = $interpolate($(clip).attr('tooltip'))();
expect(clipText).to.be('Copy to clipboard');
});
it('should change the tooltip text when clicked, back when mouse leaves', function () {
el.mouseenter();
el.click();
$scope.$digest();
var clipText = $interpolate($('[tooltip]', el).attr('tooltip'))();
expect(clipText).to.be('Copied!');
el.mouseleave();
$scope.$digest();
clipText = $interpolate($('[tooltip]', el).attr('tooltip'))();
expect(clipText).to.be('Copy to clipboard');
});
it('should unbind all handlers on destroy', function () {
var handlers = $._data(el.get(0), 'events');
expect(Object.keys(handlers)).to.have.length(2);
$scope.$destroy();
expect(Object.keys(handlers)).to.have.length(0);
});
});
});

View file

@ -1,11 +0,0 @@
<span>
<a
ng-if="!disabled"
tooltip="{{shownText}}"
tooltip-placement="{{tipPlacement}}"
tooltip-animation="false"
tooltip-popup-delay="0"
tooltip-append-to-body="true">
<i class="fa" ng-class="icon" clip-copy="copyText"></i>
</a>
</span>

View file

@ -1,47 +0,0 @@
define(function (require) {
var ZeroClipboard = require('ng-clip');
var $ = require('jquery');
var html = require('ui/clipboard/clipboard.html');
require('ui/modules')
.get('kibana')
.directive('kbnClipboard', function ($compile, $timeout) {
return {
restrict: 'E',
template: html,
replace: true,
scope: {
copyText: '=copy'
},
transclude: true,
link: function ($scope, $el, attr) {
if (ZeroClipboard.isFlashUnusable()) {
$scope.disabled = true;
return;
}
$scope.tipPlacement = attr.tipPlacement || 'top';
$scope.tipText = attr.tipText || 'Copy to clipboard';
$scope.tipConfirm = attr.tipConfirm = 'Copied!';
$scope.icon = attr.icon || 'fa-clipboard';
$scope.shownText = $scope.tipText;
$el.on('click', function () {
$scope.shownText = $scope.tipConfirm;
// Reposition tooltip to account for text length change
$('a', $el).mouseenter();
});
$el.on('mouseleave', function () {
$scope.shownText = $scope.tipText;
});
$scope.$on('$destroy', function () {
$el.off('click');
$el.off('mouseleave');
});
}
};
});
});

View file

@ -1,28 +1,33 @@
.sidebar-container.collapsible-sidebar {
@import (reference) "~ui/styles/variables";
.collapsible-sidebar {
position: relative;
z-index: 1;
.sidebar-collapser {
background-color: #ecf0f1;
height: 28px;
background-color: @collapser-bg;
height: 100%;
position: absolute;
top: 0;
right: -20px;
width: 20px;
border: 1px solid #ecf0f1;
border-width: 0 1px 1px 0;
right: -(@collapser-width);
width: @collapser-width;
cursor: pointer;
z-index: -1;
border-bottom-right-radius: 4px;
&:hover {
background-color: @collapser-hover-bg;
border-color: @collapser-hover-bg;
color: @collapser-hover-color;
}
.chevron-cont{
position: absolute;
left: 4px;
top: 5px;
left: 2px;
top: 8px;
font-size: 10px;
&:before {
font-family: FontAwesome;
color: #333;
content: "\F053";
}
}
@ -47,7 +52,7 @@
}
@media (max-width: 992px) {
.sidebar-container.collapsible-sidebar {
.collapsible-sidebar {
&.closed {
display: none;
}

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