diff --git a/.gitignore b/.gitignore index 2f5b92bc7f6f8b..af05017c9815de 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ selenium *.swp *.swo *.out -src/ui_framework/doc_site/build/*.js* +ui_framework/doc_site/build/*.js* diff --git a/docs/development/core-development.asciidoc b/docs/development/core-development.asciidoc index 5016a428606a38..2b7709f0031170 100644 --- a/docs/development/core-development.asciidoc +++ b/docs/development/core-development.asciidoc @@ -4,9 +4,12 @@ * <> * <> * <> +* <> include::core/development-basepath.asciidoc[] include::core/development-dependencies.asciidoc[] include::core/development-modules.asciidoc[] + +include::plugin/development-elasticsearch.asciidoc[] diff --git a/docs/development/plugin-development.asciidoc b/docs/development/plugin-development.asciidoc index e2d59f5b0c49d5..d86479685fafee 100644 --- a/docs/development/plugin-development.asciidoc +++ b/docs/development/plugin-development.asciidoc @@ -9,6 +9,7 @@ The Kibana plugin interfaces are in a state of constant development. We cannot * <> * <> + include::plugin/development-plugin-resources.asciidoc[] include::plugin/development-uiexports.asciidoc[] diff --git a/docs/development/plugin/development-elasticsearch.asciidoc b/docs/development/plugin/development-elasticsearch.asciidoc new file mode 100644 index 00000000000000..c4faefe2498608 --- /dev/null +++ b/docs/development/plugin/development-elasticsearch.asciidoc @@ -0,0 +1,41 @@ +[[development-elasticsearch]] +=== Communicating with Elasticsearch + +Kibana exposes two clients on the server and browser for communicating with elasticsearch. +There is an 'admin' client which is used for managing Kibana's state, and a 'data' client for all +other requests. The clients use the https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html[elasticsearch.js library]. + +[float] +[[client-server]] +=== Server clients + +Server clients are exposed through the elasticsearch plugin. +[source,javascript] +---- + const adminCluster = server.plugins.elasticsearch.getCluster('admin); + const dataCluster = server.plugins.elasticsearch.getCluster('data); + + //ping as the configured elasticsearch.user in kibana.yml + adminCluster.callWithInternalUser('ping'); + + //ping as the user specified in the current requests header + adminCluster.callWithRequest(req, 'ping'); +---- + +[float] +[[client-browser]] +=== Browser clients + +Browser clients are exposed through AngularJS services. + +[source,javascript] +---- +uiModules.get('kibana') +.run(function (esAdmin, es) { + es.ping() + .then(() => esAdmin.ping()) + .catch(err => { + console.log('error pinging servers'); + }); +}); +---- diff --git a/docs/getting-started/tutorial-define-index.asciidoc b/docs/getting-started/tutorial-define-index.asciidoc index 15fe9213ec0058..b8de9e9fe26cbe 100644 --- a/docs/getting-started/tutorial-define-index.asciidoc +++ b/docs/getting-started/tutorial-define-index.asciidoc @@ -4,7 +4,7 @@ Each set of data loaded to Elasticsearch has an index pattern. In the previous section, the Shakespeare data set has an index named `shakespeare`, and the accounts data set has an index named `bank`. An _index pattern_ is a string with optional wildcards that can match multiple indices. For example, in the common logging use -case, a typical index name contains the date in MM-DD-YYYY format, and an index pattern for May would look something +case, a typical index name contains the date in YYYY.MM.DD format, and an index pattern for May would look something like `logstash-2015.05*`. For this tutorial, any pattern that matches the name of an index we've loaded will work. Open a browser and diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 42fee52f231c92..01c0d6def71fa1 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -38,6 +38,7 @@ working on big documents. Set this property to `false` to disable highlighting. `courier:maxSegmentCount`:: Kibana splits requests in the Discover app into segments to limit the size of requests sent to the Elasticsearch cluster. This setting constrains the length of the segment list. Long segment lists can significantly increase request processing time. +`courier:ignoreFilterIfFieldNotInIndex`:: Set this property to `true` to skip filters that apply to fields that don't exist in a visualization's index. Useful when dashboards consist of visualizations from multiple index patterns. `fields:popularLimit`:: This setting governs how many of the top most popular fields are shown. `histogram:barTarget`:: When date histograms use the `auto` interval, Kibana attempts to generate this number of bars. `histogram:maxBars`:: Date histograms are not generated with more bars than the value of this property, scaling values @@ -47,16 +48,17 @@ when necessary. `visualization:tileMap:WMSdefaults`:: Default properties for the WMS map server support in the tile map. `visualization:colorMapping`:: Maps values to specified colors within visualizations. `visualization:loadingDelay`:: Time to wait before dimming visualizations during query. +`visualization:dimmingOpacity`:: When part of a visualization is highlighted, by hovering over it for example, ths is the opacity applied to the other elements. A higher number means other elements will be less opaque. `csv:separator`:: A string that serves as the separator for exported values. `csv:quoteValues`:: Set this property to `true` to quote exported values. `history:limit`:: In fields that have history, such as query inputs, the value of this property limits how many recent values are shown. -`shortDots:enable`:: Set this property to `true` to shorten long field names in visualizations. For example, instead of -`foo.bar.baz`, show `f.b.baz`. +`shortDots:enable`:: Set this property to `true` to shorten long field names in visualizations. For example, instead of `foo.bar.baz`, show `f.b.baz`. `truncate:maxHeight`:: This property specifies the maximum height that a cell occupies in a table. A value of 0 disables truncation. `indexPattern:fieldMapping:lookBack`:: The value of this property sets the number of recent matching patterns to query the field mapping for index patterns with names that contain timestamps. +`indexPattern:placeholder`:: The default placeholder value used when adding a new index pattern to Kibana. `format:defaultTypeMap`:: A map of the default format name for each field type. Field types that are not explicitly mentioned use "_default_". `format:number:defaultPattern`:: Default numeral format for the "number" format. @@ -74,3 +76,14 @@ Markdown. `notifications:lifetime:error`:: Specifies the duration in milliseconds for error notification displays. The default value is 300000. Set this field to `Infinity` to disable error notifications. `notifications:lifetime:warning`:: Specifies the duration in milliseconds for warning notification displays. The default value is 10000. Set this field to `Infinity` to disable warning notifications. `notifications:lifetime:info`:: Specifies the duration in milliseconds for information notification displays. The default value is 5000. Set this field to `Infinity` to disable information notifications. + +`timelion:showTutorial`:: Set this property to `true` to show the Timelion tutorial to users when they first open Timelion. +`timelion:es.timefield`:: Default field containing a timestamp when using the `.es()` query. +`timelion:es.default_index`:: Default index when using the `.es()` query. +`timelion:target_buckets`:: Used for calculating automatic intervals in visualizations, this is the number of buckets to try to represent. +`timelion:max_buckets`:: Used for calculating automatic intervals in visualizations, this is the maximum number of buckets to represent. +`timelion:default_columns`:: The default number of columns to use on a timelion sheet. +`timelion:default_rows`:: The default number of rows to use on a timelion sheet. +`timelion:graphite.url`:: [experimental] Used with graphite queries, this it the URL of your host +`timelion:quandl.key`:: [experimental] Used with quandl queries, this is your API key from www.quandl.com +`state:storeInSessionStorage`:: [experimental] Kibana tracks UI state in the URL, which can lead to problems when there is a lot of information there and the URL gets very long. Enabling this will store parts of the state in your browser session instead, to keep the URL shorter. \ No newline at end of file diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 14de56b7c1acac..b7129d5047b2c5 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -72,9 +72,13 @@ WARNING: Computing data on the fly with scripted fields can be very resource int Kibana's performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. -Scripted fields use the Lucene expression syntax. For more information, -see {es-ref}modules-scripting-expression.html[ -Lucene Expressions Scripts]. +When you define a scripted field in Kibana, you have a choice of scripting languages. Starting with 5.0, the default +options are {es-ref}modules-scripting-expression.html[Lucene expressions] and {es-ref}modules-scripting-painless.html[Painless]. +While you can use other scripting languages if you enable dynamic scripting for them in Elasticsearch, this is not recommended +because they cannot be sufficiently {es-ref}modules-scripting-security.html[sandboxed]. + +WARNING: Use of Groovy, Javascript, and Python scripting is deprecated starting in Elasticsearch 5.0, and support for those +scripting languages will be removed in the future. You can reference any single value numeric field in your expressions, for example: @@ -82,6 +86,9 @@ You can reference any single value numeric field in your expressions, for exampl doc['field_name'].value ---- +For more background on scripted fields and additional examples, refer to this blog: +https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in Kibana scripted fields] + [float] [[create-scripted-field]] === Creating a Scripted Field @@ -98,9 +105,6 @@ To create a scripted field: For more information about scripted fields in Elasticsearch, see {es-ref}modules-scripting.html[Scripting]. -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{es-ref}modules-scripting.html[dynamic Groovy scripting]. - [float] [[update-scripted-field]] === Updating a Scripted Field diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc index 3ffc48f9638297..2ed96266e9f383 100644 --- a/docs/setup.asciidoc +++ b/docs/setup.asciidoc @@ -56,6 +56,8 @@ include::setup/access.asciidoc[] include::setup/connect-to-elasticsearch.asciidoc[] +include::setup/tribe.asciidoc[] + include::setup/production.asciidoc[] include::setup/upgrade.asciidoc[] diff --git a/docs/setup/production.asciidoc b/docs/setup/production.asciidoc index 7fa3ab08a49139..0718705068673c 100644 --- a/docs/setup/production.asciidoc +++ b/docs/setup/production.asciidoc @@ -4,7 +4,6 @@ * <> * <> * <> -* <> How you deploy Kibana largely depends on your use case. If you are the only user, you can run Kibana on your local machine and configure it to point to whatever @@ -112,10 +111,3 @@ cluster.name: "my_cluster" # The Elasticsearch instance to use for all your queries. elasticsearch.url: "http://localhost:9200" -------- - -[float] -[[kibana-tribe]] -=== Kibana and Tribe Nodes - -Kibana 5.0 does not support tribe nodes. We are working on a solution that -addresses this limitation. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index bf31ba692dec95..178a64defa33f3 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -69,3 +69,23 @@ unauthenticated users to access the Kibana server status API and status page. `console.proxyConfig`:: A list of configuration options that are based on the proxy target. Use this to set custom timeouts or SSL settings for specific hosts. This is done by defining a set of `match` criteria using wildcards/globs which will be checked against each request. The configuration from all matching rules will then be merged together to configure the proxy used for that request. + The valid match keys are `match.protocol`, `match.host`, `match.port`, and `match.path`. All of these keys default to `*`, which means they will match any value. See <> for an example. + +`elasticsearch.tribe.url:`:: Optional URL of the Elasticsearch tribe instance to use for all your +queries. +`elasticsearch.tribe.username:` and `elasticsearch.tribe.password:`:: If your Elasticsearch is protected with basic authentication, +these settings provide the username and password that the Kibana server uses to perform maintenance on the Kibana index at +startup. Your Kibana users still need to authenticate with Elasticsearch, which is proxied through the Kibana server. +`elasticsearch.tribe.ssl.cert:` and `elasticsearch.tribe.ssl.key:`:: Optional settings that provide the paths to the PEM-format SSL +certificate and key files. These files validate that your Elasticsearch backend uses the same key files. +`elasticsearch.tribe.ssl.ca:`:: Optional setting that enables you to specify a path to the PEM file for the certificate +authority for your Elasticsearch instance. +`elasticsearch.tribe.ssl.verify:`:: *Default: true* To disregard the validity of SSL certificates, change this setting’s value +to `false`. +`elasticsearch.tribe.pingTimeout:`:: *Default: the value of the `elasticsearch.tribe.requestTimeout` setting* Time in milliseconds to +wait for Elasticsearch to respond to pings. +`elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait for responses from the back end or +Elasticsearch. This value must be a positive integer. +`elasticsearch.tribe.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List of Kibana client-side headers to send to Elasticsearch. +To send *no* client-side headers, set this value to [] (an empty list). +`elasticsearch.tribe.customHeaders:`:: *Default: `{}`* Header names and values to send to Elasticsearch. Any custom headers +cannot be overwritten by client-side headers, regardless of the `elasticsearch.tribe.requestHeadersWhitelist` configuration. diff --git a/docs/setup/tribe.asciidoc b/docs/setup/tribe.asciidoc new file mode 100644 index 00000000000000..024489b8eba66d --- /dev/null +++ b/docs/setup/tribe.asciidoc @@ -0,0 +1,33 @@ +[[tribe]] +== Using Kibana with Tribe nodes + +Kibana can be configured to connect to a https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-tribe.html[tribe node] for data retrieval. Because tribe nodes can't create indices, Kibana additionally +requires a separate connection to a node to maintain state. When configured, searches and visualizations will retrieve data using +the tribe node and administrative actions (such as saving a dashboard) will be sent to non-tribe node. + +[float] +[[tribe-configuration]] +=== Configuring Kibana for tribe nodes + +Tribe nodes take all of the same configuration options used when configuring elasticsearch in `kibana.yml`. Tribe options +are prefixed with `elasticsearch.tribe` and at a minimum requires a url: +[source,text] +---- +elasticsearch.url: "" +elasticsearch.tribe.url: "" +---- + +When configured to use a tribe node, actions that modify Kibana's state will be sent to the node at `elasticsearch.url`. Searches and visualizations +will retrieve data from the node at `elasticsearch.tribe.url`. It's acceptable to use a node for `elasticsearch.url` that is part of one of the clusters that +a tribe node is pointing to. + +The full list of configurations can be found at <>. + +[float] +[[tribe-limitations]] +=== Limitations + +Due to the ambiguity of which cluster is being used, certain features are disabled in Kibana: + +* Console +* Managing users and roles with the x-pack plugin diff --git a/package.json b/package.json index 4efe427fdfe696..abda4e729ba1b9 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "mocha": "mocha", "mocha:debug": "mocha --debug-brk", "sterilize": "grunt sterilize", - "uiFramework:start": "webpack-dev-server --config src/ui_framework/doc_site/webpack.config.js --hot --inline --content-base src/ui_framework/doc_site/build" + "uiFramework:start": "grunt uiFramework:start" }, "repository": { "type": "git", @@ -145,7 +145,6 @@ "moment-timezone": "0.5.4", "no-ui-slider": "1.2.0", "node-fetch": "1.3.2", - "node-sass": "3.8.0", "node-uuid": "1.4.7", "pegjs": "0.9.0", "postcss-loader": "1.2.1", @@ -155,7 +154,6 @@ "rimraf": "2.4.3", "rison-node": "1.0.0", "rjs-repack-loader": "1.0.6", - "sass-loader": "4.0.0", "script-loader": "0.6.1", "semver": "5.1.0", "style-loader": "0.12.3", @@ -223,6 +221,7 @@ "mocha": "2.5.3", "murmurhash3js": "3.0.1", "ncp": "2.0.0", + "node-sass": "3.8.0", "nock": "8.0.0", "npm": "3.10.8", "portscanner": "1.0.0", @@ -244,6 +243,7 @@ "react-select": "^1.0.0-rc.1", "react-sortable": "^1.1.0", "reactcss": "^1.0.7", + "sass-loader": "4.0.0", "simple-git": "1.37.0", "simianhacker-react-resize-aware": "^1.0.11", "sinon": "1.17.2", diff --git a/src/cli/serve/__tests__/read_yaml_config.js b/src/cli/serve/__tests__/read_yaml_config.js index 29b620b27dbb00..cc674a7e9fdade 100644 --- a/src/cli/serve/__tests__/read_yaml_config.js +++ b/src/cli/serve/__tests__/read_yaml_config.js @@ -17,7 +17,7 @@ describe('cli/serve/read_yaml_config', function () { }); }); - it('reads and merged mulitple config file', function () { + it('reads and merged multiple config file', function () { const config = readYamlConfig([ fixture('one.yml'), fixture('two.yml') diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 853553ad8f01e2..9f018fa045d04b 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -77,7 +77,7 @@ module.exports = function (program) { .option( '-c, --config ', 'Path to the config file, can be changed with the CONFIG_PATH environment variable as well. ' + - 'Use mulitple --config args to include multiple config files.', + 'Use multiple --config args to include multiple config files.', configPathCollector, [ getConfig() ] ) diff --git a/src/core_plugins/console/index.js b/src/core_plugins/console/index.js index a5d0865791fb8a..7b7bec418fc731 100644 --- a/src/core_plugins/console/index.js +++ b/src/core_plugins/console/index.js @@ -175,7 +175,7 @@ export default function (kibana) { uiExports: { apps: apps, - + hacks: ['plugins/console/hacks/register'], devTools: ['plugins/console/console'], injectDefaultVars(server, options) { diff --git a/src/core_plugins/console/public/console.js b/src/core_plugins/console/public/console.js index f1bdee908459e4..5733823b5c5fbb 100644 --- a/src/core_plugins/console/public/console.js +++ b/src/core_plugins/console/public/console.js @@ -1,4 +1,3 @@ -import devTools from 'ui/registry/dev_tools'; import uiRoutes from 'ui/routes'; import template from './index.html'; @@ -16,12 +15,6 @@ require('./src/directives/sense_settings'); require('./src/directives/sense_help'); require('./src/directives/sense_welcome'); -devTools.register(() => ({ - order: 1, - name: 'console', - display: 'Console', - url: '#/dev_tools/console' -})); uiRoutes.when('/dev_tools/console', { controller: 'SenseController', diff --git a/src/core_plugins/console/public/hacks/register.js b/src/core_plugins/console/public/hacks/register.js new file mode 100644 index 00000000000000..1476a6d47d1cba --- /dev/null +++ b/src/core_plugins/console/public/hacks/register.js @@ -0,0 +1,7 @@ +import devTools from 'ui/registry/dev_tools'; +devTools.register(() => ({ + order: 1, + name: 'console', + display: 'Console', + url: '#/dev_tools/console' +})); diff --git a/src/core_plugins/elasticsearch/lib/__tests__/routes.js b/src/core_plugins/elasticsearch/lib/__tests__/routes.js index 5ff257829e51d9..6691b579330ac2 100644 --- a/src/core_plugins/elasticsearch/lib/__tests__/routes.js +++ b/src/core_plugins/elasticsearch/lib/__tests__/routes.js @@ -36,7 +36,7 @@ describe('plugins/elasticsearch', function () { } describe(format('%s %s', options.method, options.url), function () { - it('should should return ' + statusCode, function (done) { + it('should return ' + statusCode, function (done) { kbnTestServer.makeRequest(kbnServer, options, function (res) { if (res.statusCode === statusCode) { done(); diff --git a/src/core_plugins/kbn_vislib_vis_types/public/heatmap.js b/src/core_plugins/kbn_vislib_vis_types/public/heatmap.js index 718aa327553177..53316d79529c24 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/heatmap.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/heatmap.js @@ -65,7 +65,7 @@ export default function HeatmapVisType(Private) { title: 'Value', min: 1, max: 1, - aggFilter: ['count', 'avg', 'median', 'sum', 'min', 'max', 'cardinality', 'std_dev'], + aggFilter: ['count', 'avg', 'median', 'sum', 'min', 'max', 'cardinality', 'std_dev', 'top_hits'], defaults: [ { schema: 'metric', type: 'count' } ] diff --git a/src/core_plugins/kbn_vislib_vis_types/public/line.js b/src/core_plugins/kbn_vislib_vis_types/public/line.js index 0576ec45a9a6b6..2f74f681a5d3db 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/line.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/line.js @@ -70,7 +70,7 @@ export default function HistogramVisType(Private) { title: 'Dot Size', min: 0, max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'] }, { group: 'buckets', diff --git a/src/core_plugins/kbn_vislib_vis_types/public/pie.js b/src/core_plugins/kbn_vislib_vis_types/public/pie.js index 0947e957b09a6d..6ec170695fdc99 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/pie.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/pie.js @@ -44,7 +44,7 @@ export default function HistogramVisType(Private) { title: 'Slice Size', min: 1, max: 1, - aggFilter: ['sum', 'count', 'cardinality'], + aggFilter: ['sum', 'count', 'cardinality', 'top_hits'], defaults: [ { schema: 'metric', type: 'count' } ] diff --git a/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js b/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js index 6605479c0798c5..9ec130f84737f9 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js @@ -89,7 +89,7 @@ export default function TileMapVisType(Private, getAppState, courier, config) { title: 'Value', min: 1, max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'], defaults: [ { schema: 'metric', type: 'count' } ] diff --git a/src/core_plugins/kibana/index.js b/src/core_plugins/kibana/index.js index 447dc00f578abc..b44039bda3f1f5 100644 --- a/src/core_plugins/kibana/index.js +++ b/src/core_plugins/kibana/index.js @@ -26,6 +26,7 @@ module.exports = function (kibana) { }, uiExports: { + hacks: ['plugins/kibana/dev_tools/hacks/hide_empty_tools'], app: { id: 'kibana', title: 'Kibana', @@ -43,10 +44,24 @@ module.exports = function (kibana) { ], injectVars: function (server, options) { - const config = server.config(); + const serverConfig = server.config(); + + //DEPRECATED SETTINGS + //if the url is set, the old settings must be used. + //keeping this logic for backward compatibilty. + const configuredUrl = server.config().get('tilemap.url'); + const isOverridden = typeof configuredUrl === 'string' && configuredUrl !== ''; + const tilemapConfig = serverConfig.get('tilemap'); + return { - kbnDefaultAppId: config.get('kibana.defaultAppId'), - tilemap: config.get('tilemap') + kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'), + tilemapsConfig: { + deprecated: { + isOverridden: isOverridden, + config: tilemapConfig, + }, + manifestServiceUrl: serverConfig.get('tilemap.manifestServiceUrl') + }, }; }, }, diff --git a/src/core_plugins/kibana/public/dashboard/components/panel/lib/panel_state.js b/src/core_plugins/kibana/public/dashboard/components/panel/lib/panel_state.js index 1e8a2286cbfb84..e1b53822b0267e 100644 --- a/src/core_plugins/kibana/public/dashboard/components/panel/lib/panel_state.js +++ b/src/core_plugins/kibana/public/dashboard/components/panel/lib/panel_state.js @@ -1,5 +1,5 @@ -export const DEFAULT_PANEL_WIDTH = 3; -export const DEFAULT_PANEL_HEIGHT = 2; +export const DEFAULT_PANEL_WIDTH = 6; +export const DEFAULT_PANEL_HEIGHT = 3; /** * Represents a panel on a grid. Keeps track of position in the grid and what visualization it diff --git a/src/core_plugins/kibana/public/dashboard/components/panel/panel.html b/src/core_plugins/kibana/public/dashboard/components/panel/panel.html index 1cba6ed1d7ca5f..c38cf45a2bd31c 100644 --- a/src/core_plugins/kibana/public/dashboard/components/panel/panel.html +++ b/src/core_plugins/kibana/public/dashboard/components/panel/panel.html @@ -4,13 +4,16 @@ {{::savedObj.title}} diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js b/src/core_plugins/kibana/public/dashboard/dashboard_constants.js new file mode 100644 index 00000000000000..107a972409ab0b --- /dev/null +++ b/src/core_plugins/kibana/public/dashboard/dashboard_constants.js @@ -0,0 +1,5 @@ + +export const DashboardConstants = { + ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM: 'addToDashboard', + NEW_VISUALIZATION_ID_PARAM: 'addVisualization' +}; diff --git a/src/core_plugins/kibana/public/dashboard/directives/dashboard_panel.js b/src/core_plugins/kibana/public/dashboard/directives/dashboard_panel.js index e0ce4a49965172..a974683872cc04 100644 --- a/src/core_plugins/kibana/public/dashboard/directives/dashboard_panel.js +++ b/src/core_plugins/kibana/public/dashboard/directives/dashboard_panel.js @@ -59,7 +59,17 @@ uiModules * as saved searches. We need to remove reliance there before we can break it out here. * See https://github.com/elastic/kibana/issues/9558 for more information. */ - state: '=' + state: '=', + /** + * Expand or collapse the current panel, so it either takes up the whole screen or goes back to its + * natural size. + * @type {function} + */ + toggleExpand: '&', + /** + * @type {boolean} + */ + isExpanded: '=' }, link: function ($scope, element) { if (!$scope.panel.id || !$scope.panel.type) return; diff --git a/src/core_plugins/kibana/public/dashboard/directives/grid.js b/src/core_plugins/kibana/public/dashboard/directives/grid.js index 476aaa8f50c355..c7ec6a3b625f72 100644 --- a/src/core_plugins/kibana/public/dashboard/directives/grid.js +++ b/src/core_plugins/kibana/public/dashboard/directives/grid.js @@ -162,11 +162,14 @@ app.directive('dashboardGrid', function ($compile, Notifier) { const panelHtml = `
  • - +
  • `; panel.$el = $compile(panelHtml)($scope); diff --git a/src/core_plugins/kibana/public/dashboard/get_top_nav_config.js b/src/core_plugins/kibana/public/dashboard/get_top_nav_config.js new file mode 100644 index 00000000000000..d98a15ddaed048 --- /dev/null +++ b/src/core_plugins/kibana/public/dashboard/get_top_nav_config.js @@ -0,0 +1,89 @@ + +/** + * @param kbnUrl - used to change the url. + * @return {Array} - Returns an array of objects for a top nav configuration. + * Note that order matters and the top nav will be displayed in the same order. + */ +export function getTopNavConfig(kbnUrl) { + return [ + getNewConfig(kbnUrl), + getAddConfig(), + getSaveConfig(), + getOpenConfig(), + getShareConfig(), + getOptionsConfig()]; +} + +/** + * + * @param kbnUrl + * @returns {kbnTopNavConfig} + */ +function getNewConfig(kbnUrl) { + return { + key: 'new', + description: 'New Dashboard', + testId: 'dashboardNewButton', + run: () => { kbnUrl.change('/dashboard', {}); } + }; +} + +/** + * @returns {kbnTopNavConfig} + */ +function getAddConfig() { + return { + key: 'add', + description: 'Add a panel to the dashboard', + testId: 'dashboardAddPanelButton', + template: require('plugins/kibana/dashboard/partials/pick_visualization.html') + }; +} + +/** + * @returns {kbnTopNavConfig} + */ +function getSaveConfig() { + return { + key: 'save', + description: 'Save Dashboard', + testId: 'dashboardSaveButton', + template: require('plugins/kibana/dashboard/partials/save_dashboard.html') + }; +} + +/** + * @returns {kbnTopNavConfig} + */ +function getOpenConfig() { + return { + key: 'open', + description: 'Open Saved Dashboard', + testId: 'dashboardOpenButton', + template: require('plugins/kibana/dashboard/partials/load_dashboard.html') + }; +} + +/** + * @returns {kbnTopNavConfig} + */ +function getShareConfig() { + return { + key: 'share', + description: 'Share Dashboard', + testId: 'dashboardShareButton', + template: require('plugins/kibana/dashboard/partials/share.html') + }; +} + +/** + * @returns {kbnTopNavConfig} + */ +function getOptionsConfig() { + return { + key: 'options', + description: 'Options', + testId: 'dashboardOptionsButton', + template: require('plugins/kibana/dashboard/partials/options.html') + }; +} diff --git a/src/core_plugins/kibana/public/dashboard/index.html b/src/core_plugins/kibana/public/dashboard/index.html index 4acb39da039db8..562ec85c737557 100644 --- a/src/core_plugins/kibana/public/dashboard/index.html +++ b/src/core_plugins/kibana/public/dashboard/index.html @@ -10,7 +10,7 @@ > @@ -54,10 +54,18 @@ -
    +

    Ready to get started?

    Click the Add button in the menu bar above to add a visualization to the dashboard.
    If you haven't setup a visualization yet visit the "Visualize" tab to create your first visualization.

    - + + + diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index f9e4b3587bda1a..478854e564058d 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -17,7 +17,10 @@ import uiRoutes from 'ui/routes'; import uiModules from 'ui/modules'; import indexTemplate from 'plugins/kibana/dashboard/index.html'; import { savedDashboardRegister } from 'plugins/kibana/dashboard/services/saved_dashboard_register'; +import { getTopNavConfig } from './get_top_nav_config'; import { createPanelState } from 'plugins/kibana/dashboard/components/panel/lib/panel_state'; +import { DashboardConstants } from './dashboard_constants'; + require('ui/saved_objects/saved_object_registry').register(savedDashboardRegister); const app = uiModules.get('app/dashboard', [ @@ -104,37 +107,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, $scope.$watch('state.options.darkTheme', setDarkTheme); - $scope.topNavMenu = [{ - key: 'new', - description: 'New Dashboard', - run: function () { kbnUrl.change('/dashboard', {}); }, - testId: 'dashboardNewButton', - }, { - key: 'add', - description: 'Add a panel to the dashboard', - template: require('plugins/kibana/dashboard/partials/pick_visualization.html'), - testId: 'dashboardAddPanelButton', - }, { - key: 'save', - description: 'Save Dashboard', - template: require('plugins/kibana/dashboard/partials/save_dashboard.html'), - testId: 'dashboardSaveButton', - }, { - key: 'open', - description: 'Open Saved Dashboard', - template: require('plugins/kibana/dashboard/partials/load_dashboard.html'), - testId: 'dashboardOpenButton', - }, { - key: 'share', - description: 'Share Dashboard', - template: require('plugins/kibana/dashboard/partials/share.html'), - testId: 'dashboardShareButton', - }, { - key: 'options', - description: 'Options', - template: require('plugins/kibana/dashboard/partials/options.html'), - testId: 'dashboardOptionsButton', - }]; + $scope.topNavMenu = getTopNavConfig(kbnUrl); $scope.refresh = _.bindKey(courier, 'fetch'); @@ -208,6 +181,17 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, chrome.addApplicationClass(theme); } + $scope.expandedPanel = null; + $scope.hasExpandedPanel = () => $scope.expandedPanel !== null; + $scope.toggleExpandPanel = (panelIndex) => { + if ($scope.expandedPanel && $scope.expandedPanel.panelIndex === panelIndex) { + $scope.expandedPanel = null; + } else { + $scope.expandedPanel = + $scope.state.panels.find((panel) => panel.panelIndex === panelIndex); + } + }; + // update root source when filters update $scope.$listen(queryFilter, 'update', function () { updateQueryOnRootSource(); @@ -217,6 +201,10 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, // update data when filters fire fetch event $scope.$listen(queryFilter, 'fetch', $scope.refresh); + $scope.getDashTitle = function () { + return dash.lastSavedTitle; + }; + $scope.newDashboard = function () { kbnUrl.change('/dashboard', {}); }; @@ -275,6 +263,16 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, $state.panels.push(createPanelState(hit.id, 'visualization', getMaxPanelIndex())); }; + if ($route.current.params && $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]) { + $scope.addVis({ id: $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM] }); + kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); + kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM); + } + + const addNewVis = function addNewVis() { + kbnUrl.change(`/visualize?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`); + }; + $scope.addSearch = function (hit) { pendingVis++; $state.panels.push(createPanelState(hit.id, 'search', getMaxPanelIndex())); @@ -286,11 +284,16 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, ui: $state.options, save: $scope.save, addVis: $scope.addVis, + addNewVis, addSearch: $scope.addSearch, timefilter: $scope.timefilter }; init(); + + $scope.showEditHelpText = () => { + return !$scope.state.panels.length; + }; } }; }); diff --git a/src/core_plugins/kibana/public/dashboard/partials/pick_visualization.html b/src/core_plugins/kibana/public/dashboard/partials/pick_visualization.html index fd5eb0b7881ec5..664b4eada813a1 100644 --- a/src/core_plugins/kibana/public/dashboard/partials/pick_visualization.html +++ b/src/core_plugins/kibana/public/dashboard/partials/pick_visualization.html @@ -18,10 +18,16 @@
    - + +
    - +
    diff --git a/src/core_plugins/kibana/public/dashboard/styles/main.less b/src/core_plugins/kibana/public/dashboard/styles/main.less index a4b61f1e09ea76..f7a480a6574571 100644 --- a/src/core_plugins/kibana/public/dashboard/styles/main.less +++ b/src/core_plugins/kibana/public/dashboard/styles/main.less @@ -36,6 +36,10 @@ dashboard-grid { .gs-w { border: 2px dashed transparent; + .panel .panel-heading .btn-group { + display: none; + } + &:hover { border-color: @kibanaGray4; @@ -54,101 +58,110 @@ dashboard-grid { i.remove { cursor: pointer; } +} - dashboard-panel { - display: block; - height: 100%; - background: @dashboard-panel-bg; - color: @dashboard-panel-color; - padding: 0; - overflow: hidden; - position: relative; - - .panel { - margin: 0; - // maintain the 100% height of the panel - height: 100%; - - // flex layout allows us to define the visualize element as "fill available space" - display: flex; - flex-direction: column; - justify-content: flex-start; - border: 0 solid transparent; +.dashboard-container { + flex: 1; + display: flex; + flex-direction: column; +} - .panel-heading { - padding: 0px 0px 0px 5px; - flex: 0 0 auto; - white-space: nowrap; - display: flex; - border-top-right-radius: 0; - border-top-left-radius: 0; - background-color: @white; - border: none; +dashboard-panel { + flex: 1; + display: flex; + flex-direction: column; - .btn-group { - a { - color: inherit; - } - display: none; - white-space: nowrap; - flex: 0 0 auto; - } + height: 100%; + background: @dashboard-panel-bg; + color: @dashboard-panel-color; + padding: 0; + overflow: hidden; + position: relative; - .panel-title { - font-size: inherit; + .panel { + margin: 0; + // maintain the 100% height of the panel + height: 100%; + flex: 1; + + // flex layout allows us to define the visualize element as "fill available space" + display: flex; + flex-direction: column; + justify-content: flex-start; + border: 0 solid transparent; + + .panel-heading { + padding: 0px 0px 0px 5px; + flex: 0 0 auto; + white-space: nowrap; + display: flex; + border-top-right-radius: 0; + border-top-left-radius: 0; + background-color: @white; + border: none; - // flexbox fix for IE10 - // http://stackoverflow.com/questions/22008135/internet-explorer-10-does-not-apply-flexbox-on-inline-elements - display: inline-block; + .btn-group { + a { + color: inherit; + } + white-space: nowrap; + flex: 0 0 auto; + } - .ellipsis(); - flex: 1 1 auto; + .panel-title { + font-size: inherit; - i { - opacity: 0.3; - font-size: 1.2em; - margin-right: 4px; - } - } + // flexbox fix for IE10 + // http://stackoverflow.com/questions/22008135/internet-explorer-10-does-not-apply-flexbox-on-inline-elements + display: inline-block; - .panel-move:hover { - cursor: move; - } + .ellipsis(); + flex: 1 1 auto; - a { - color: @dashboard-panel-heading-link-color; - border: none; - background: none; - padding: 0px 3px; - &:hover { - color: @dashboard-panel-heading-link-hover-color; - } + i { + opacity: 0.3; + font-size: 1.2em; + margin-right: 4px; } } - .visualize-show-spy { - visibility: hidden; + .panel-move:hover { + cursor: move; } - .load-error { - text-align: center; - font-size: 1em; - display: flex; - flex: 1 0 auto; - justify-content: center; - flex-direction: column; - - .fa-exclamation-triangle { - font-size: 2em; - color: @dashboard-panel-load-error-color; + a { + color: @dashboard-panel-heading-link-color; + border: none; + background: none; + padding: 0px 3px; + &:hover { + color: @dashboard-panel-heading-link-hover-color; } } + } - .panel-content { - display: flex; - flex: 1 1 100%; - height: auto; + .visualize-show-spy { + visibility: hidden; + } + + .load-error { + text-align: center; + font-size: 1em; + display: flex; + flex: 1 0 auto; + justify-content: center; + flex-direction: column; + + .fa-exclamation-triangle { + font-size: 2em; + color: @dashboard-panel-load-error-color; } } + + .panel-content { + display: flex; + flex: 1 1 100%; + height: auto; + } } } diff --git a/src/core_plugins/kibana/public/dev_tools/lib/__tests__/hide_empty_tools.js b/src/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js similarity index 100% rename from src/core_plugins/kibana/public/dev_tools/lib/__tests__/hide_empty_tools.js rename to src/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js diff --git a/src/core_plugins/kibana/public/dev_tools/lib/hide_empty_tools.js b/src/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js similarity index 79% rename from src/core_plugins/kibana/public/dev_tools/lib/hide_empty_tools.js rename to src/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js index 2b605f8fdf861b..42d6adcc4ee191 100644 --- a/src/core_plugins/kibana/public/dev_tools/lib/hide_empty_tools.js +++ b/src/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js @@ -1,3 +1,4 @@ +import modules from 'ui/modules'; import chrome from 'ui/chrome'; import DevToolsRegistryProvider from 'ui/registry/dev_tools'; @@ -8,3 +9,5 @@ export function hideEmptyDevTools(Private) { navLink.hidden = true; } } + +modules.get('kibana').run(hideEmptyDevTools); diff --git a/src/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js b/src/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js index e248be72b4f2c1..aedd64d2d8250b 100644 --- a/src/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js +++ b/src/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js @@ -41,6 +41,15 @@ describe('fieldCalculator', function () { .to.throwError(); }); + it('should handle values with dots in them', function () { + values = ['0', '0.........', '0.......,.....']; + params = {}; + groups = fieldCalculator._groupValues(values, params); + expect(groups[values[0]].count).to.be(1); + expect(groups[values[1]].count).to.be(1); + expect(groups[values[2]].count).to.be(1); + }); + it('should have a a key for value in the array when not grouping array terms', function () { expect(_.keys(groups).length).to.be(3); expect(groups.foo).to.be.a(Object); diff --git a/src/core_plugins/kibana/public/discover/components/field_chooser/lib/field_calculator.js b/src/core_plugins/kibana/public/discover/components/field_chooser/lib/field_calculator.js index a819ac58cd02fe..1f78b3610eae5d 100644 --- a/src/core_plugins/kibana/public/discover/components/field_chooser/lib/field_calculator.js +++ b/src/core_plugins/kibana/public/discover/components/field_chooser/lib/field_calculator.js @@ -80,7 +80,7 @@ function _groupValues(allValues, params) { } _.each(k, function (key) { - if (_.has(groups, key)) { + if (groups.hasOwnProperty(key)) { groups[key].count++; } else { groups[key] = { diff --git a/src/core_plugins/kibana/public/kibana.js b/src/core_plugins/kibana/public/kibana.js index 4a5e059c90efbd..5815cc33c2d151 100644 --- a/src/core_plugins/kibana/public/kibana.js +++ b/src/core_plugins/kibana/public/kibana.js @@ -20,7 +20,6 @@ import 'ui/agg_types'; import 'ui/timepicker'; import Notifier from 'ui/notify/notifier'; import 'leaflet'; -import { hideEmptyDevTools } from './dev_tools/lib/hide_empty_tools'; routes.enable(); @@ -50,4 +49,3 @@ chrome }); modules.get('kibana').run(Notifier.pullMessageFromUrl); -modules.get('kibana').run(hideEmptyDevTools); diff --git a/src/core_plugins/kibana/public/management/index.js b/src/core_plugins/kibana/public/management/index.js index bc61a4c14be858..79d720ae13a108 100644 --- a/src/core_plugins/kibana/public/management/index.js +++ b/src/core_plugins/kibana/public/management/index.js @@ -7,7 +7,6 @@ import 'ui/field_editor'; import 'plugins/kibana/management/sections/indices/_indexed_fields'; import 'plugins/kibana/management/sections/indices/_scripted_fields'; import 'plugins/kibana/management/sections/indices/source_filters/source_filters'; -import 'ui/directives/bread_crumbs'; import uiRoutes from 'ui/routes'; import uiModules from 'ui/modules'; import appTemplate from 'plugins/kibana/management/app.html'; diff --git a/src/core_plugins/kibana/public/management/sections/indices/_create.js b/src/core_plugins/kibana/public/management/sections/indices/_create.js index 050187efdeb2a6..0c070892a99a90 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/_create.js +++ b/src/core_plugins/kibana/public/management/sections/indices/_create.js @@ -22,8 +22,7 @@ uiModules.get('apps/management') // this and child scopes will write pattern vars here const index = $scope.index = { - name: 'logstash-*', - + name: config.get('indexPattern:placeholder'), isTimeBased: true, nameIsPattern: false, notExpandable: false, diff --git a/src/core_plugins/kibana/public/management/sections/indices/_field_editor.js b/src/core_plugins/kibana/public/management/sections/indices/_field_editor.js index b0d6cb51aa4e8a..cd4ba685e8acaa 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/_field_editor.js +++ b/src/core_plugins/kibana/public/management/sections/indices/_field_editor.js @@ -6,7 +6,7 @@ import uiRoutes from 'ui/routes'; import fieldEditorTemplate from 'plugins/kibana/management/sections/indices/_field_editor.html'; uiRoutes -.when('/management/kibana/indices/:indexPatternId/field/:fieldName', { mode: 'edit' }) +.when('/management/kibana/indices/:indexPatternId/field/:fieldName*', { mode: 'edit' }) .when('/management/kibana/indices/:indexPatternId/create-field/', { mode: 'create' }) .defaults(/management\/kibana\/indices\/[^\/]+\/(field|create-field)(\/|$)/, { template: fieldEditorTemplate, diff --git a/src/core_plugins/kibana/public/management/sections/indices/_indexed_fields.html b/src/core_plugins/kibana/public/management/sections/indices/_indexed_fields.html index 682abf6449f228..de1f269dc4ef44 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/_indexed_fields.html +++ b/src/core_plugins/kibana/public/management/sections/indices/_indexed_fields.html @@ -1,7 +1,8 @@ + per-page="perPage" + show-blank-rows="false">

    No matching fields found.

    diff --git a/src/core_plugins/kibana/public/management/sections/indices/_scripted_fields.html b/src/core_plugins/kibana/public/management/sections/indices/_scripted_fields.html index 7687a70eb95a60..3a80493ee61ab4 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/_scripted_fields.html +++ b/src/core_plugins/kibana/public/management/sections/indices/_scripted_fields.html @@ -14,7 +14,8 @@

    Scripted fields

    + per-page="perPage" + show-blank-rows="false">

    No matching scripted fields found.

    diff --git a/src/core_plugins/kibana/public/management/sections/indices/source_filters/source_filters.html b/src/core_plugins/kibana/public/management/sections/indices/source_filters/source_filters.html index f75f1c4b6a1c78..e246de9b088e2b 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/source_filters/source_filters.html +++ b/src/core_plugins/kibana/public/management/sections/indices/source_filters/source_filters.html @@ -32,6 +32,7 @@

    Source Filters

    + per-page="perPage" + show-blank-rows="false"> diff --git a/src/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/core_plugins/kibana/public/management/sections/objects/_objects.js index 07614b03b58f4b..a49d28db45edb9 100644 --- a/src/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -37,6 +37,7 @@ uiModules.get('apps/management') const getData = function (filter) { const services = registry.all().map(function (obj) { const service = $injector.get(obj.service); + return service.find(filter).then(function (data) { return { service: service, diff --git a/src/core_plugins/kibana/public/visualize/editor/agg_params.js b/src/core_plugins/kibana/public/visualize/editor/agg_params.js index 12db500fd7cadd..c1b9baf3c5f293 100644 --- a/src/core_plugins/kibana/public/visualize/editor/agg_params.js +++ b/src/core_plugins/kibana/public/visualize/editor/agg_params.js @@ -82,11 +82,18 @@ uiModules // build collection of agg params html type.params.forEach(function (param, i) { let aggParam; + let fields; + // if field param exists, compute allowed fields + if (param.name === 'field') { + fields = $aggParamEditorsScope.indexedFields; + } else if (param.type === 'field') { + fields = $aggParamEditorsScope[`${param.name}Options`] = param.getFieldOptions($scope.agg); + } - if ($aggParamEditorsScope.indexedFields) { - const hasIndexedFields = $aggParamEditorsScope.indexedFields.length > 0; + if (fields) { + const hasIndexedFields = fields.length > 0; const isExtraParam = i > 0; - if (!hasIndexedFields && isExtraParam) { // don't draw the rest of the options if their are no indexed fields. + if (!hasIndexedFields && isExtraParam) { // don't draw the rest of the options if there are no indexed fields. return; } } diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.js b/src/core_plugins/kibana/public/visualize/editor/editor.js index 9853f09b2f23ba..0a4716f1029889 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/core_plugins/kibana/public/visualize/editor/editor.js @@ -5,6 +5,7 @@ import 'plugins/kibana/visualize/editor/agg_filter'; import 'ui/visualize'; import 'ui/collapsible_sidebar'; import 'ui/share'; +import chrome from 'ui/chrome'; import angular from 'angular'; import Notifier from 'ui/notify/notifier'; import RegistryVisTypesProvider from 'ui/registry/vis_types'; @@ -16,6 +17,7 @@ import stateMonitorFactory from 'ui/state_management/state_monitor_factory'; import uiRoutes from 'ui/routes'; import uiModules from 'ui/modules'; import editorTemplate from 'plugins/kibana/visualize/editor/editor.html'; +import { DashboardConstants } from 'plugins/kibana/dashboard/dashboard_constants'; uiRoutes .when('/visualize/create', { @@ -63,7 +65,7 @@ uiModules }; }); -function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $timeout, courier, Private, Promise) { +function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courier, Private, Promise) { const docTitle = Private(DocTitleProvider); const brushEvent = Private(UtilsBrushEventProvider); const queryFilter = Private(FilterBarQueryFilterProvider); @@ -177,13 +179,19 @@ function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $tim $scope.uiState = $state.makeStateful('uiState'); $scope.appStatus = $appStatus; + const addToDashMode = $route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM]; + kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); + + $scope.isAddToDashMode = () => addToDashMode; + // Associate PersistedState instance with the Vis instance, so that // `uiStateVal` can be called on it. Currently this is only used to extract // map-specific information (e.g. mapZoom, mapCenter). vis.setUiState($scope.uiState); + $scope.timefilter = timefilter; - $scope.opts = _.pick($scope, 'doSave', 'savedVis', 'shareData', 'timefilter'); + $scope.opts = _.pick($scope, 'doSave', 'savedVis', 'shareData', 'timefilter', 'isAddToDashMode'); stateMonitor = stateMonitorFactory.create($state, stateDefaults); stateMonitor.ignoreProps([ 'vis.listeners' ]).onChange((status) => { @@ -304,7 +312,13 @@ function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $tim if (id) { notify.info('Saved Visualization "' + savedVis.title + '"'); - if (savedVis.id === $route.current.params.id) { + if ($scope.isAddToDashMode()) { + const dashboardBaseUrl = chrome.getNavLinkById('kibana:dashboard'); + // Not using kbnUrl.change here because the dashboardBaseUrl is a full path, not a url suffix. + // Rather than guess the right substring, we'll just navigate there directly, just as if the user + // clicked the dashboard link in the UI. + $window.location.href = `${dashboardBaseUrl.lastSubUrl}&${DashboardConstants.NEW_VISUALIZATION_ID_PARAM}=${savedVis.id}`; + } else if (savedVis.id === $route.current.params.id) { docTitle.change(savedVis.lastSavedTitle); } else { kbnUrl.change('/visualize/edit/{{id}}', { id: savedVis.id }); diff --git a/src/core_plugins/kibana/public/visualize/editor/panels/save.html b/src/core_plugins/kibana/public/visualize/editor/panels/save.html index 982b77d3c56c62..59b4355e87f48c 100644 --- a/src/core_plugins/kibana/public/visualize/editor/panels/save.html +++ b/src/core_plugins/kibana/public/visualize/editor/panels/save.html @@ -19,6 +19,6 @@ type="submit" class="btn btn-primary" > - Save + {{ opts.isAddToDashMode() ? 'Save and Add to Dashboard' : 'Save'}} diff --git a/src/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js b/src/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js index 35464682d86df2..714b1850ccfc55 100644 --- a/src/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js +++ b/src/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js @@ -1,5 +1,3 @@ -import _ from 'lodash'; -import Scanner from 'ui/utils/scanner'; import 'plugins/kibana/visualize/saved_visualizations/_saved_vis'; import RegistryVisTypesProvider from 'ui/registry/vis_types'; import uiModules from 'ui/modules'; diff --git a/src/core_plugins/kibana/public/visualize/wizard/wizard.js b/src/core_plugins/kibana/public/visualize/wizard/wizard.js index b7bc6910e9ed1e..98fb790a502648 100644 --- a/src/core_plugins/kibana/public/visualize/wizard/wizard.js +++ b/src/core_plugins/kibana/public/visualize/wizard/wizard.js @@ -1,8 +1,9 @@ -import _ from 'lodash'; + import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations'; import 'ui/directives/saved_object_finder'; import 'ui/directives/paginated_selectable_list'; import 'plugins/kibana/discover/saved_searches/saved_searches'; +import { DashboardConstants } from 'plugins/kibana/dashboard/dashboard_constants'; import routes from 'ui/routes'; import RegistryVisTypesProvider from 'ui/registry/vis_types'; import uiModules from 'ui/modules'; @@ -21,13 +22,21 @@ routes.when('/visualize/step/1', { template: templateStep(1, require('plugins/kibana/visualize/wizard/step_1.html')) }); -module.controller('VisualizeWizardStep1', function ($scope, $route, $location, timefilter, Private) { +module.controller('VisualizeWizardStep1', function ($scope, $route, kbnUrl, timefilter, Private) { timefilter.enabled = false; + const addToDashMode = $route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM]; + kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); + $scope.visTypes = Private(RegistryVisTypesProvider); $scope.visTypeUrl = function (visType) { - if (!visType.requiresSearch) return '#/visualize/create?type=' + encodeURIComponent(visType.name); - else return '#/visualize/step/2?type=' + encodeURIComponent(visType.name); + const baseUrl = visType.requiresSearch ? '#/visualize/step/2?' : '#/visualize/create?'; + const params = [`type=${encodeURIComponent(visType.name)}`]; + if (addToDashMode) { + params.push(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); + } + + return baseUrl + params.join('&'); }; }); @@ -43,10 +52,17 @@ routes.when('/visualize/step/2', { } }); -module.controller('VisualizeWizardStep2', function ($route, $scope, $location, timefilter, kbnUrl) { +module.controller('VisualizeWizardStep2', function ($route, $scope, timefilter, kbnUrl) { const type = $route.current.params.type; + const addToDashMode = $route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM]; + kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); $scope.step2WithSearchUrl = function (hit) { + if (addToDashMode) { + return kbnUrl.eval( + `#/visualize/create?&type={{type}}&savedSearchId={{id}}&${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`, + { type: type, id: hit.id }); + } return kbnUrl.eval('#/visualize/create?&type={{type}}&savedSearchId={{id}}', { type: type, id: hit.id }); }; @@ -59,6 +75,10 @@ module.controller('VisualizeWizardStep2', function ($route, $scope, $location, t $scope.makeUrl = function (pattern) { if (!pattern) return; + + if (addToDashMode) { + return `#/visualize/create?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}&type=${type}&indexPattern=${pattern}`; + } return `#/visualize/create?type=${type}&indexPattern=${pattern}`; }; }); diff --git a/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js b/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js index 2d9515db7bd7fe..c07b3cfc4bd334 100644 --- a/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js +++ b/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js @@ -21,14 +21,7 @@ describe('metric vis', function () { $scope.processTableGroups({ tables: [{ columns: [{ title: 'Count' }], - rows: [[4301021]], - aggConfig: function () { - return { - fieldFormatter: function () { - return formatter; - } - }; - } + rows: [[ { toString: () => formatter(4301021) } ]] }] }); @@ -44,14 +37,7 @@ describe('metric vis', function () { { title: '1st percentile of bytes' }, { title: '99th percentile of bytes' } ], - rows: [[182, 445842.4634666484]], - aggConfig: function () { - return { - fieldFormatter: function () { - return formatter; - } - }; - } + rows: [[ { toString: () => formatter(182) }, { toString: () => formatter(445842.4634666484) } ]] }] }); diff --git a/src/core_plugins/metric_vis/public/metric_vis_controller.js b/src/core_plugins/metric_vis/public/metric_vis_controller.js index c9efb01d417a9c..e3fddf42a36a24 100644 --- a/src/core_plugins/metric_vis/public/metric_vis_controller.js +++ b/src/core_plugins/metric_vis/public/metric_vis_controller.js @@ -17,14 +17,11 @@ module.controller('KbnMetricVisController', function ($scope, $element, Private) $scope.processTableGroups = function (tableGroups) { tableGroups.tables.forEach(function (table) { table.columns.forEach(function (column, i) { - const fieldFormatter = table.aggConfig(column).fieldFormatter(); - let value = table.rows[0][i]; - - value = isInvalid(value) ? '?' : fieldFormatter(value); + const value = table.rows[0][i]; metrics.push({ label: column.title, - value: value + value: value.toString('html') }); }); }); @@ -32,8 +29,12 @@ module.controller('KbnMetricVisController', function ($scope, $element, Private) $scope.$watch('esResponse', function (resp) { if (resp) { + const options = { + asAggConfigResults: true + }; + metrics.length = 0; - $scope.processTableGroups(tabifyAggResponse($scope.vis, resp)); + $scope.processTableGroups(tabifyAggResponse($scope.vis, resp, options)); $element.trigger('renderComplete'); } }); diff --git a/src/core_plugins/tagcloud/public/__tests__/tag_cloud.js b/src/core_plugins/tagcloud/public/__tests__/tag_cloud.js index 30dcfce012afba..2fe354421c4f53 100644 --- a/src/core_plugins/tagcloud/public/__tests__/tag_cloud.js +++ b/src/core_plugins/tagcloud/public/__tests__/tag_cloud.js @@ -440,9 +440,12 @@ describe('tag cloud tests', function () { const centered = (largest[1] === 0 && largest[2] === 0); + const halfWidth = debugInfo.size.width / 2; + const halfHeight = debugInfo.size.height / 2; const inside = debugInfo.positions.filter(position => { - return debugInfo.size[0] <= position[1] && position[1] <= debugInfo.size[0] - && debugInfo.size[1] <= position[2] && position[2] <= debugInfo.size[1]; + const x = position.x + halfWidth; + const y = position.y + halfHeight; + return 0 <= x && x <= debugInfo.size.width && 0 <= y && y <= debugInfo.size.height; }); return centered && inside.length === count - 1; @@ -452,7 +455,6 @@ describe('tag cloud tests', function () { function handleExpectedBlip(assertion) { return function () { if (!shouldAssert()) { - console.warn('Skipping assertion.'); return; } assertion(); diff --git a/src/core_plugins/tagcloud/public/tag_cloud.js b/src/core_plugins/tagcloud/public/tag_cloud.js index dfb9507d6ae846..7a0f56e8ca723a 100644 --- a/src/core_plugins/tagcloud/public/tag_cloud.js +++ b/src/core_plugins/tagcloud/public/tag_cloud.js @@ -307,8 +307,18 @@ class TagCloud extends EventEmitter { */ getDebugInfo() { const debug = {}; - debug.positions = this._currentJob ? this._currentJob.words.map(tag => [tag.text, tag.x, tag.y, tag.rotate]) : []; - debug.size = this._size.slice(); + debug.positions = this._currentJob ? this._currentJob.words.map(tag => { + return { + text: tag.text, + x: tag.x, + y: tag.y, + rotate: tag.rotate + }; + }) : []; + debug.size = { + width: this._size[0], + height: this._size[1] + }; return debug; } diff --git a/src/core_plugins/tests_bundle/tests_entry_template.js b/src/core_plugins/tests_bundle/tests_entry_template.js index e56be5ffa709e9..f5208e78de347c 100644 --- a/src/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/core_plugins/tests_bundle/tests_entry_template.js @@ -28,13 +28,19 @@ window.__KBN__ = { esShardTimeout: 1500, esApiVersion: 'master', esRequestTimeout: '300000', - tilemap: { - url: 'https://tiles.elastic.co/v1/default/{z}/{x}/{y}.png?my_app_name=kibana&my_app_version=1.2.3&elastic_tile_service_tos=agree', - options: { - minZoom: 1, - maxZoom: 10, - attribution: '© [Elastic Tile Service](https://www.elastic.co/elastic_tile_service)' - } + tilemapsConfig: { + deprecated: { + isOverridden: true, + config: { + url: 'https://tiles.elastic.co/v1/default/{z}/{x}/{y}.png?my_app_name=kibana&my_app_version=1.2.3&elastic_tile_service_tos=agree', + options: { + minZoom: 1, + maxZoom: 10, + attribution: '© [Elastic Tile Service](https://www.elastic.co/elastic_tile_service)' + } + } + }, + manifestServiceUrl: 'https://proxy-tiles.elastic.co/v1/manifest' } }, uiSettings: { diff --git a/src/core_plugins/timelion/public/partials/docs/tutorial.html b/src/core_plugins/timelion/public/partials/docs/tutorial.html index 4a936f01f19068..2759a5f316b1ba 100644 --- a/src/core_plugins/timelion/public/partials/docs/tutorial.html +++ b/src/core_plugins/timelion/public/partials/docs/tutorial.html @@ -1,186 +1,396 @@
    -
    +
    -

    Welcome to timelion the timeseries expression interface for everything

    -

    - Timelion. Timeline. Get it? Ok, enough with the puns. Timelion is the, clawing, gnashing, zebra killing, pluggable timeseries interface for everything. If your datastore can produce a timeseries, then you have all of the awesome power of Timelion at your disposal. Timelion lets you compare, combine and combobulate (not actually a word) datasets across multiple data sources, even entirely different technologies, all with the same easy-to-master expression syntax. While the beginning of this tutorial will focus on Elasticsearch, once you're rolling you'll discover you can use nearly everything you learn here with any datasource timelion supports. -

    - -

    - Why start with elasticsearch? Well, you're using timelion, so we know you have Kibana, so you definitely have Elasticsearch. So the answer is: Because its easy. Timelion want everything to be easy. Ok, let's do this thing. If you're already familar with Timelion's syntax, Jump to the function reference, otherwise click the Next button in the lower right corner. -

    +

    Welcome to Timelion!

    +

    + Timelion is the clawing, gnashing, zebra killing, pluggable time + series interface for everything. If your datastore can + produce a time series, then you have all of the awesome power of + Timelion at your disposal. Timelion lets you compare, combine, and + combobulate datasets across multiple datasources with one + easy-to-master expression syntax. This tutorial focuses on + Elasticsearch, but you'll quickly discover that what you learn here + applies to any datasource Timelion supports. +

    +

    + Ready to get started? Click Next. Want to skip the + tutorial and view the docs? + Jump to the function reference. +

    -
    - - + +
    -
    -
    +
    +
    -

    First time configuration

    - If you're using logstash you're already done. Otherwise, - pop over to Kibana's "Advanced Settigns" and update timelion:es.timefield and timelion:es.default_index parameters to match your environment. + If you're using Logstash, you don't need to configure anything to + start exploring your log data with Timelion. To search other + indices, go to Management / Kibana / Advanced Settings + and configure the timelion:es.default_index + and timelion:es.timefield settings to match your + indices.

    -

    - You'll see some other timelion parameters in there too, we won't be messing with them for now, but you can probably guess what they do. And frankly, most can be specified on-the-fly with the timelion expression syntax. More on that in a bit. + You'll also see some other Timelion settings. For now, you don't need + to worry about them. Later, you'll see that you can set most of + them on the fly if you need to.

    - + - Could not validate elasticsearch settings: {{es.invalidReason}}. Check your Advanced Settings and try again. ({{es.invalidCount}}) + Could not validate Elasticsearch settings: + {{es.invalidReason}}. Check your Advanced Settings + and try again. ({{es.invalidCount}}) - +
    -

    Good news Elasticsearch is configured correctly!

    +

    Good news, Elasticsearch is configured correctly!

    - Or at least, things look ok. I validated your default index and your timefield and everything looks ok. Given your settings I found data between {{es.stats.min}} and {{es.stats.max}}. You're probably all set. If this doesn't look right, Click here for instructions on configuring the elasticsearch data source. + We validated your default index and your timefield and everything + looks ok. We found data from {{es.stats.min}} to + {{es.stats.max}}. You're probably all set. If this + doesn't look right, see First time + configuration for information about configuring the Elasticsearch + datasource.

    -

    Intervals

    - You might already have one nice chart, but I'm going to operate on the assumption you don't for educational purposes. The input bar at the top has two inputs. On the left, is your expression, leave that alone for now, we'll get to it. On the right is the interval selector, which is currently set to {{state.interval}}. - Looks good! - Set it to auto. - If timelion thinks your combination of time range and interval will produce too many data points it will throw an error. You can configure that limit in Advanced Settings + should already see one chart, but you might need to make a + couple adjustments before you see any interesting data:

    +
      +
    • + Intervals +

      + The interval selector at the right of the input bar lets you + control the sampling frequency. It's currently set to + {{state.interval}}. + + You're all set! + + + Set it to auto to let Timelion choose an + appropriate interval. + + If Timelion thinks your combination of time range and interval + will produce too many data points, it throws an error. You can + adjust that limit by configuring timelion:max_buckets + in Management/Kibana/Advanced Settings. +

      +
    • +
    • + Time range +

      + Use the timepicker in the + Kibana toolbar to select the time period that contains the + data you want to visualize. Make sure you select a time + period that includes all or part of the time range shown + above. +

      +
    • +

    -

    Time range

    - Now see that clock icon in the top right? Click it and select a time period that includes all or part of the time range in the first paragraph above. If you didn't before, you should now have a line chart with a count of your data points over time. + Now, you should see a line chart that displays a count of your + data points over time.

    - - + +
    -
    -

    Elasticsearch querying in short

    +

    Querying the Elasticsearch datasource

    - We're going to start off talking about the Elasticsearch datasource, because we've already validated that one works for you. Enter .es(*) in the expression input, if its not there already. Hit enter. + Now that we've validated that you have a working Elasticsearch + datasource, you can start submitting queries. For starters, + enter .es(*) in the input bar and hit enter.

    - This said "hey elasticsearch, find everything in my default index". If you wanted to find a subset you might do something like .es(html) to count events matching html, or .es('user:bob AND bytes:>100') to find events with bob in the user field, and a bytes field that is greater than 100. Note that we surrounded our query in single quotes this time, because it has spaces. You can enter any lucene query string as the first argument to the .es() function. - + This says hey Elasticsearch, find everything in my default + index. If you want to find a subset, you could enter something + like .es(html) to count events that match html, + or .es('user:bob AND bytes:>100') to find events + that contain bob in the user field and have a + bytes field that is greater than 100. Note that this query + is enclosed in single quotes—that's because it contains + spaces. You can enter any + + Lucene query string + + as the first argument to the .es() function. +

    +

    Passing arguments

    -

    Passing arguments

    - Timelion has a number of shortcuts for doing common things, one of which is that for simple arguments, ones that don't contain spaces or special characters, you don't need quotes. Many functions also have defaults, for example .es() and .es(*) do the same thing. Arguments also have names, so you don't have to remember their position, you can pass .es(index='logstash-*', q='*') to tell the elasticsearch data source "use * as the q (query) for the logstash-* index" + Timelion has a number of shortcuts that make it easy to do common + things. One is that for simple arguments that don't contain spaces or + special characters, you don't need to use quotes. Many functions also + have defaults. For example, .es() and .es(*) + do the same thing. Arguments also have names, so you don't have to + specify them in a specific order. For example, you can enter + .es(index='logstash-*', q='*') to tell the + Elasticsearch datasource use * as the q (query) for the + logstash-* index.

    Beyond count

    - Counting events is all well and good, but the elasticsearch data source also supports any Elasticsearch metric that returns a single value. Min, max, avg, sum and cardinality are some of the most useful. Let's say you want a unique count of the src_ip field. You could do say, .es(*, metric='cardinality:src_ip'). To get the average of the bytes field you would run: .es(metric='avg:bytes'). + Counting events is all well and good, but the Elasticsearch datasource + also supports any + + Elasticsearch metric aggregation + + that returns a single value. Some of the most useful are + min, max, avg, sum, + and cardinality. Let's say you want a unique count of the + src_ip field. Simply use the cardinality + metric: .es(*, metric='cardinality:src_ip'). To get the + average of the bytes field, you can use the + avg metric: .es(metric='avg:bytes').

    - - + +
    -

    Expressions and expressing yourself

    +

    Expressing yourself with expressions

    - Every timelion expression starts with a datasource function. From there, the sky is the limit and new functions can be appended, or "chained", to the data source to transform and augment it. From here we're going to assume you know something about your data. Feel free to replace the elasticsearch query with something more meaningful to you. + Every expression starts with a datasource function. From there, you + can append new functions to the datasource to transform and augment + it.

    -

    - Up until now we've dealt with just the one chart. We're going to experiment, so add a few more. Click the Menu icon in the top right to expand the menu. Then click the Add Chart button. - - - - - - - - - - - - - - - - - - - - - -
    .es(*)One expression
    .es(*), .es(US)Two expressions. Two expressions on the same chart!
    .es(*).color(#f66), .es(US).bars(1)Red expression. Let's colorize the first series red instead. Also, instead of lines for 2nd series, we'll have some bars, with a 1 pixel width.
    .es(*).color(#f66).lines(fill=3), .es(US).bars(1).points(radius=3, weight=1)Wooo expressions. In the last example we used un-named arguments to color() and bars, which rely on the arguments position in a comma separated list. We can use named arguments to make expressions easier to read and arguments easier to remember.
    (.es(*), .es(GB)).points()Also grouped expressions. Groups of expressions can be chained to functions as well. Both series will be shown as points instead of lines.
    + By the way, from here on out you probably know more about your data + than we do. Feel free to replace the sample queries with something + more meaningful! +

    +

    + We're going to experiment, so click Add in the Kibana + toolbar to add another chart or three. Then, select a chart, copy + one of the following expressions, paste it into the input bar, + and hit enter. Rinse, repeat to try out the other expressions. +

    + + + + + + + + + + + + + + + + + +
    .es(*), .es(US)Double the fun. Two expressions on the same + chart.
    .es(*).color(#f66), .es(US).bars(1) + Custom styling. Colorizes the first series red + and uses 1 pixel wide bars for the second series. +
    + .es(*).color(#f66).lines(fill=3), + .es(US).bars(1).points(radius=3, weight=1) + + Named arguments. Forget trying to remember what + order you need to specify arguments in, use named arguments to make + the expressions easier to read and write. +
    (.es(*), .es(GB)).points() + Grouped expressions. You can also chain groups + of expressions to functions. Here, both series are shown as + points instead of lines. +
    +

    + Timelion provides additional view transformation functions you can use + to customize the appearance of your charts. For the complete list, see + the Function reference.

    -
    - - + +
    -
    -

    Data: Transform insert beat boxing

    +

    Transforming your data: the real fun begins!

    - We can make our charts pretty all day, but its time for businessing. As an example exercise, we're going to figure out what percentage some subset of our data represents of the whole, over time. For example, what percentage of my web traffic comes from the US? Let's start with finding all events that contain US: .es('US'). Now, to find that ratio to the whole, we'd need to divide 'US' by everything, try this: .es('US').divide(.es()). Ah, not bad, but of course this provides us with a number between 0 and 1, let's correct that to a percentage: .es('US').divide(.es()).multiply(100). There, now we've divided all US traffic by all worldwide traffic, then multiplied the result by 100 to get a percentage. + Now that you've mastered the basics, it's time to unleash the power of + Timelion. Let's figure out what percentage some subset of our data + represents of the whole, over time. For example, what percentage of + our web traffic comes from the US? +

    +

    + First, we need to find all events that contain US: + .es('US'). +

    +

    + Next, we want to calculate the ratio of US events to the whole. To + divide 'US' by everything, we can use the + divide function: .es('US').divide(.es()). +

    +

    + Not bad, but this gives us a number between 0 and 1. To convert it + to a percentage, simply multiply by 100: + .es('US').divide(.es()).multiply(100). +

    +

    + Now we know what percentage of our traffic comes from the US, and + can see how it has changed over time! + Timelion has a number of built-in arithmetic functions, such as + sum, subtract, multiply, and + divide. Many of these can take a series or a number. + There are also other useful data transformation functions, such as + movingaverage, abs, and + derivative. +

    +

    Now that you're familiar with the syntax, refer to the + Function reference to see + how to use all of the available Timelion functions.

    -

    Timelion has a number of built in arithmetic functions, such as sum, subtract, multiply and divide, many of which can take a series or a number. There are also other data transformation functions including movingaverage, abs and derivative. In addition there are other view transformation functions than the ones we learned on the previous page. See the function reference for the complete list of transforming, and drawing functions. - -

    Now that you know the syntax, jump over to the Function Reference for detailed info on all of Timelions available functions.

    - - Tip: You can always find this again by clicking the in the menu - + + + Tip: You can view the Function reference at any + time by clicking Docs in the Kibana toolbar. To + get back to this tutorial, click the Tutorial link at the top of + the Function reference. + +
    -
    Function reference
    - Click a function for details and arguments or return to the tutorial. + Click any function for more information. Just getting started? + Check out the tutorial.
    - + - - diff --git a/src/core_plugins/timelion/public/services/dashboard_context.js b/src/core_plugins/timelion/public/services/dashboard_context.js index a499e30bd180c5..d089ad6713ddfa 100644 --- a/src/core_plugins/timelion/public/services/dashboard_context.js +++ b/src/core_plugins/timelion/public/services/dashboard_context.js @@ -21,8 +21,10 @@ module.exports = function dashboardContext(Private, getAppState) { if (filter.meta.disabled) return; if (filter.meta.negate) { + bool.must_not = bool.must_not || []; bool.must_not.push(esFilter.query || esFilter); } else { + bool.must = bool.must || []; bool.must.push(esFilter.query || esFilter); } }); diff --git a/src/core_plugins/timelion/server/series_functions/es/lib/build_request.js b/src/core_plugins/timelion/server/series_functions/es/lib/build_request.js index e30c6859264c18..c57443c00034e0 100644 --- a/src/core_plugins/timelion/server/series_functions/es/lib/build_request.js +++ b/src/core_plugins/timelion/server/series_functions/es/lib/build_request.js @@ -3,7 +3,7 @@ const createDateAgg = require('./create_date_agg'); module.exports = function buildRequest(config, tlConfig) { - const bool = { must: [], must_not: [] }; + const bool = { must: [] }; const timeFilter = { range:{} }; timeFilter.range[config.timefield] = { gte: tlConfig.time.from, lte: tlConfig.time.to, format: 'epoch_millis' }; @@ -11,7 +11,7 @@ module.exports = function buildRequest(config, tlConfig) { // Use the kibana filter bar filters if (config.kibana) { - bool.filter = _.get(tlConfig, 'request.payload.extended.es.filter') || {}; + bool.filter = _.get(tlConfig, 'request.payload.extended.es.filter'); } const aggs = { diff --git a/src/fixtures/logstash_fields.js b/src/fixtures/logstash_fields.js index 4cdfdd43d4aa7e..b44d822a6fd031 100644 --- a/src/fixtures/logstash_fields.js +++ b/src/fixtures/logstash_fields.js @@ -5,7 +5,7 @@ function stubbedLogstashFields() { // | | |aggregatable // | | | |searchable // name type | | | | |metadata - ['bytes', 'number', true, true, true, true, { count: 10 } ], + ['bytes', 'number', true, true, true, true, { count: 10, docValues: true } ], ['ssl', 'boolean', true, true, true, true, { count: 20 } ], ['@timestamp', 'date', true, true, true, true, { count: 30 } ], ['time', 'date', true, true, true, true, { count: 30 } ], @@ -20,6 +20,7 @@ function stubbedLogstashFields() { ['geo.coordinates', 'geo_point', true, true, true, true ], ['extension', 'string', true, true, true, true ], ['machine.os', 'string', true, true, true, true ], + ['machine.os.raw', 'string', true, false, true, true, { docValues: true } ], ['geo.src', 'string', true, true, true, true ], ['_id', 'string', false, false, true, true ], ['_type', 'string', false, false, true, true ], @@ -41,6 +42,7 @@ function stubbedLogstashFields() { ] = row; const { + docValues = false, count = 0, script, lang = script ? 'expression' : undefined, @@ -50,6 +52,7 @@ function stubbedLogstashFields() { return { name, type, + doc_values: docValues, indexed, analyzed, aggregatable, diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 41a74a085e0423..0fea18a79910b8 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -71,18 +71,18 @@ class BaseOptimizer { const makeStyleLoader = preprocessor => { let loaders = [ - loaderWithSourceMaps('css-loader') + loaderWithSourceMaps('css-loader?autoprefixer=false'), + { + name: 'postcss-loader', + query: { + config: require.resolve('./postcss.config') + } + }, ]; if (preprocessor) { loaders = [ ...loaders, - { - name: 'postcss-loader', - query: { - config: require.resolve('./postcss.config') - } - }, loaderWithSourceMaps(preprocessor) ]; } @@ -125,7 +125,6 @@ class BaseOptimizer { module: { loaders: [ { test: /\.less$/, loader: makeStyleLoader('less-loader') }, - { test: /\.scss$/, loader: makeStyleLoader('sass-loader') }, { test: /\.css$/, loader: makeStyleLoader() }, { test: /\.jade$/, loader: 'jade-loader' }, { test: /\.json$/, loader: 'json-loader' }, diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 957afe2f869ce5..7fe2f6effe3c7d 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -131,11 +131,11 @@ module.exports = () => Joi.object({ status: Joi.object({ allowAnonymous: Joi.boolean().default(false) }).default(), - tilemap: Joi.object({ - url: Joi.string().default(`https://tiles.elastic.co/v1/default/{z}/{x}/{y}.png?my_app_name=kibana&my_app_version=${pkg.version}&elastic_tile_service_tos=agree`), + manifestServiceUrl: Joi.string().default('https://proxy-tiles.elastic.co/v1/manifest'), + url: Joi.string(), options: Joi.object({ - attribution: Joi.string().default('© [Elastic Tile Service](https://www.elastic.co/elastic-tile-service)'), + attribution: Joi.string(), minZoom: Joi.number().min(1, 'Must not be less than 1').default(1), maxZoom: Joi.number().default(10), tileSize: Joi.number(), @@ -146,7 +146,6 @@ module.exports = () => Joi.object({ bounds: Joi.array().items(Joi.array().items(Joi.number()).min(2).required()).min(2) }).default() }).default(), - uiSettings: Joi.object({ // this is used to prevent the uiSettings from initializing. Since they // require the elasticsearch plugin in order to function we need to turn diff --git a/src/test_utils/stub_index_pattern.js b/src/test_utils/stub_index_pattern.js index 64ff63c3d6d5cc..c14a08a30c50ac 100644 --- a/src/test_utils/stub_index_pattern.js +++ b/src/test_utils/stub_index_pattern.js @@ -8,6 +8,7 @@ import getComputedFields from 'ui/index_patterns/_get_computed_fields'; import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; import IndexPatternsFlattenHitProvider from 'ui/index_patterns/_flatten_hit'; import IndexPatternsFieldProvider from 'ui/index_patterns/_field'; + export default function (Private) { const fieldFormats = Private(RegistryFieldFormatsProvider); const flattenHit = Private(IndexPatternsFlattenHitProvider); diff --git a/src/ui/public/agg_response/hierarchical/_create_raw_data.js b/src/ui/public/agg_response/hierarchical/_create_raw_data.js index 9b786fe2b0a45f..0bcdbf27ecf010 100644 --- a/src/ui/public/agg_response/hierarchical/_create_raw_data.js +++ b/src/ui/public/agg_response/hierarchical/_create_raw_data.js @@ -53,6 +53,7 @@ export default function (vis, resp) { * @returns {void} */ function walkBuckets(agg, data, record) { + if (!data) return; if (!_.isArray(record)) { record = []; } diff --git a/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js b/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js index 820b32db26580b..85632260e1d3ac 100644 --- a/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js +++ b/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js @@ -18,7 +18,6 @@ export default function buildHierarchicalDataProvider(Private, Notifier) { // Create a refrenece to the buckets let buckets = vis.aggs.bySchemaGroup.buckets; - // Find the metric so it's easier to reference. // TODO: Change this to support multiple metrics. const metric = vis.aggs.bySchemaGroup.metrics[0]; @@ -51,7 +50,7 @@ export default function buildHierarchicalDataProvider(Private, Notifier) { } const firstAgg = buckets[0]; - const aggData = resp.aggregations[firstAgg.id]; + const aggData = resp.aggregations ? resp.aggregations[firstAgg.id] : null; if (!firstAgg._next && firstAgg.schema.name === 'split') { notify.error('Splitting charts without splitting slices is not supported. Pretending that we are just splitting slices.'); diff --git a/src/ui/public/agg_types/__tests__/buckets/_terms.js b/src/ui/public/agg_types/__tests__/buckets/_terms.js deleted file mode 100644 index ce117c7d96b08d..00000000000000 --- a/src/ui/public/agg_types/__tests__/buckets/_terms.js +++ /dev/null @@ -1,13 +0,0 @@ -describe('Terms Agg', function () { - describe('order agg editor UI', function () { - it('defaults to the first metric agg'); - it('adds "custom metric" option'); - it('lists all metric agg responses'); - it('lists individual values of a multi-value metric'); - it('selects "custom metric" if there are no metric aggs'); - it('is emptied if the selected metric is removed'); - it('displays a metric editor if "custom metric" is selected'); - it('saves the "custom metric" to state and refreshes from it'); - it('invalidates the form if the metric agg form is not complete'); - }); -}); diff --git a/src/ui/public/agg_types/__tests__/buckets/terms.js b/src/ui/public/agg_types/__tests__/buckets/terms.js new file mode 100644 index 00000000000000..d5fd937b0b3d89 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/buckets/terms.js @@ -0,0 +1,156 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypesIndexProvider from 'ui/agg_types/index'; + +describe('Terms Agg', function () { + describe('order agg editor UI', function () { + + let $rootScope; + + function init({ responseValueAggs = [] }) { + ngMock.module('kibana'); + ngMock.inject(function (Private, $controller, _$rootScope_) { + const terms = Private(AggTypesIndexProvider).byName.terms; + const orderAggController = terms.params.byName.orderAgg.controller; + + $rootScope = _$rootScope_; + $rootScope.agg = { + id: 'test', + params: {}, + type: terms, + vis: { + aggs: [] + } + }; + $rootScope.responseValueAggs = responseValueAggs; + $controller(orderAggController, { $scope: $rootScope }); + $rootScope.$digest(); + }); + } + + it('defaults to the first metric agg', function () { + init({ + responseValueAggs: [ + { + id: 'agg1', + type: { + name: 'count' + } + }, + { + id: 'agg2', + type: { + name: 'count' + } + } + ] + }); + expect($rootScope.agg.params.orderBy).to.be('agg1'); + }); + + it('defaults to the first metric agg that is compatible with the terms bucket', function () { + init({ + responseValueAggs: [ + { + id: 'agg1', + type: { + name: 'top_hits' + } + }, + { + id: 'agg2', + type: { + name: 'percentiles' + } + }, + { + id: 'agg3', + type: { + name: 'median' + } + }, + { + id: 'agg4', + type: { + name: 'std_dev' + } + }, + { + id: 'agg5', + type: { + name: 'count' + } + } + ] + }); + expect($rootScope.agg.params.orderBy).to.be('agg5'); + }); + + it('defaults to the _term metric if no agg is compatible', function () { + init({ + responseValueAggs: [ + { + id: 'agg1', + type: { + name: 'top_hits' + } + } + ] + }); + expect($rootScope.agg.params.orderBy).to.be('_term'); + }); + + it('selects _term if there are no metric aggs', function () { + init({}); + expect($rootScope.agg.params.orderBy).to.be('_term'); + }); + + it('selects _term if the selected metric becomes incompatible', function () { + init({ + responseValueAggs: [ + { + id: 'agg1', + type: { + name: 'count' + } + } + ] + }); + expect($rootScope.agg.params.orderBy).to.be('agg1'); + $rootScope.responseValueAggs = [ + { + id: 'agg1', + type: { + name: 'top_hits' + } + } + ]; + $rootScope.$digest(); + expect($rootScope.agg.params.orderBy).to.be('_term'); + }); + + it('selects _term if the selected metric is removed', function () { + init({ + responseValueAggs: [ + { + id: 'agg1', + type: { + name: 'count' + } + } + ] + }); + expect($rootScope.agg.params.orderBy).to.be('agg1'); + $rootScope.responseValueAggs = []; + $rootScope.$digest(); + expect($rootScope.agg.params.orderBy).to.be('_term'); + }); + + it('adds "custom metric" option'); + it('lists all metric agg responses'); + it('lists individual values of a multi-value metric'); + it('displays a metric editor if "custom metric" is selected'); + it('saves the "custom metric" to state and refreshes from it'); + it('invalidates the form if the metric agg form is not complete'); + }); +}); diff --git a/src/ui/public/agg_types/__tests__/metrics/top_hit.js b/src/ui/public/agg_types/__tests__/metrics/top_hit.js new file mode 100644 index 00000000000000..04429a447787c5 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/metrics/top_hit.js @@ -0,0 +1,342 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import TopHitProvider from 'ui/agg_types/metrics/top_hit'; +import VisProvider from 'ui/vis'; +import StubbedIndexPattern from 'fixtures/stubbed_logstash_index_pattern'; + +describe('Top hit metric', function () { + let aggDsl; + let topHitMetric; + let aggConfig; + + function init({ field, sortOrder = 'desc', aggregate = 'concat', size = 1 }) { + ngMock.module('kibana'); + ngMock.inject(function (Private) { + const Vis = Private(VisProvider); + const indexPattern = Private(StubbedIndexPattern); + topHitMetric = Private(TopHitProvider); + + const params = {}; + if (field) { + params.field = field; + } + params.sortOrder = { + val: sortOrder + }; + params.aggregate = { + val: aggregate + }; + params.size = size; + const vis = new Vis(indexPattern, { + title: 'New Visualization', + type: 'metric', + params: { + fontSize: 60, + handleNoResults: true + }, + aggs: [ + { + id: '1', + type: 'top_hits', + schema: 'metric', + params + } + ], + listeners: {} + }); + + // Grab the aggConfig off the vis (we don't actually use the vis for anything else) + aggConfig = vis.aggs[0]; + aggDsl = aggConfig.toDsl(); + }); + } + + it('should return a label prefixed with Last if sorting in descending order', function () { + init({ field: 'bytes' }); + expect(topHitMetric.makeLabel(aggConfig)).to.eql('Last bytes'); + }); + + it('should return a label prefixed with First if sorting in ascending order', function () { + init({ + field: 'bytes', + sortOrder: 'asc' + }); + expect(topHitMetric.makeLabel(aggConfig)).to.eql('First bytes'); + }); + + it('should request the _source field', function () { + init({ field: '_source' }); + expect(aggDsl.top_hits._source).to.be(true); + expect(aggDsl.top_hits.docvalue_fields).to.be(undefined); + }); + + it('should request both for the source and doc_values fields', function () { + init({ field: 'bytes' }); + expect(aggDsl.top_hits._source).to.be('bytes'); + expect(aggDsl.top_hits.docvalue_fields).to.eql([ 'bytes' ]); + }); + + it('should only request for the source if the field does not have the doc_values property', function () { + init({ field: 'ssl' }); + expect(aggDsl.top_hits._source).to.be('ssl'); + expect(aggDsl.top_hits.docvalue_fields).to.be(undefined); + }); + + describe('try to get the value from the top hit', function () { + it('should return null if there is no hit', function () { + const bucket = { + '1': { + hits: { + hits: [] + } + } + }; + + init({ field: '@tags' }); + expect(topHitMetric.getValue(aggConfig, bucket)).to.be(null); + }); + + it('should return undefined if the field does not appear in the source', function () { + const bucket = { + '1': { + hits: { + hits: [ + { + _source: { + bytes: 123 + } + } + ] + } + } + }; + + init({ field: '@tags' }); + expect(topHitMetric.getValue(aggConfig, bucket)).to.be(undefined); + }); + + it('should return the field value from the top hit', function () { + const bucket = { + '1': { + hits: { + hits: [ + { + _source: { + '@tags': 'aaa' + } + } + ] + } + } + }; + + init({ field: '@tags' }); + expect(topHitMetric.getValue(aggConfig, bucket)).to.be('aaa'); + }); + + it('should return the object if the field value is an object', function () { + const bucket = { + '1': { + hits: { + hits: [ + { + _source: { + '@tags': { + label: 'aaa' + } + } + } + ] + } + } + }; + + init({ field: '@tags' }); + expect(topHitMetric.getValue(aggConfig, bucket)).to.eql({ label: 'aaa' }); + }); + + it('should return an array if the field has more than one values', function () { + const bucket = { + '1': { + hits: { + hits: [ + { + _source: { + '@tags': [ 'aaa', 'bbb' ] + } + } + ] + } + } + }; + + init({ field: '@tags' }); + expect(topHitMetric.getValue(aggConfig, bucket)).to.eql([ 'aaa', 'bbb' ]); + }); + + it('should get the value from the doc_values field if the source does not have that field', function () { + const bucket = { + '1': { + hits: { + hits: [ + { + _source: { + 'machine.os': 'linux' + }, + fields: { + 'machine.os.raw': [ 'linux' ] + } + } + ] + } + } + }; + + init({ field: 'machine.os.raw' }); + expect(topHitMetric.getValue(aggConfig, bucket)).to.be('linux'); + }); + + it('should return undefined if the field is not in the source nor in the doc_values field', function () { + const bucket = { + '1': { + hits: { + hits: [ + { + _source: { + bytes: 12345 + }, + fields: { + bytes: 12345 + } + } + ] + } + } + }; + + init({ field: 'machine.os.raw' }); + expect(topHitMetric.getValue(aggConfig, bucket)).to.be(undefined); + }); + + describe('Multivalued field and first/last X docs', function () { + it('should return a label prefixed with Last X docs if sorting in descending order', function () { + init({ + field: 'bytes', + size: 2 + }); + expect(topHitMetric.makeLabel(aggConfig)).to.eql('Last 2 bytes'); + }); + + it('should return a label prefixed with First X docs if sorting in ascending order', function () { + init({ + field: 'bytes', + size: 2, + sortOrder: 'asc' + }); + expect(topHitMetric.makeLabel(aggConfig)).to.eql('First 2 bytes'); + }); + + [ + { + description: 'concat values with a comma', + type: 'concat', + data: [ 1, 2, 3 ], + result: [ 1, 2, 3 ] + }, + { + description: 'sum up the values', + type: 'sum', + data: [ 1, 2, 3 ], + result: 6 + }, + { + description: 'take the minimum value', + type: 'min', + data: [ 1, 2, 3 ], + result: 1 + }, + { + description: 'take the maximum value', + type: 'max', + data: [ 1, 2, 3 ], + result: 3 + }, + { + description: 'take the average value', + type: 'average', + data: [ 1, 2, 3 ], + result: 2 + }, + { + description: 'support null/undefined', + type: 'min', + data: [ undefined, null ], + result: null + }, + { + description: 'support null/undefined', + type: 'max', + data: [ undefined, null ], + result: null + }, + { + description: 'support null/undefined', + type: 'sum', + data: [ undefined, null ], + result: null + }, + { + description: 'support null/undefined', + type: 'average', + data: [ undefined, null ], + result: null + } + ] + .forEach(agg => { + it(`should return the result of the ${agg.type} aggregation over the last doc - ${agg.description}`, function () { + const bucket = { + '1': { + hits: { + hits: [ + { + _source: { + bytes: agg.data + } + } + ] + } + } + }; + + init({ field: 'bytes', aggregate: agg.type }); + expect(topHitMetric.getValue(aggConfig, bucket)).to.eql(agg.result); + }); + + it(`should return the result of the ${agg.type} aggregation over the last X docs - ${agg.description}`, function () { + const bucket = { + '1': { + hits: { + hits: [ + { + _source: { + bytes: _.dropRight(agg.data, 1) + } + }, + { + _source: { + bytes: _.last(agg.data) + } + } + ] + } + } + }; + + init({ field: 'bytes', aggregate: agg.type }); + expect(topHitMetric.getValue(aggConfig, bucket)).to.eql(agg.result); + }); + }); + }); + }); +}); diff --git a/src/ui/public/agg_types/__tests__/param_types/_field.js b/src/ui/public/agg_types/__tests__/param_types/_field.js index cf9927ef19ffac..72550f3461f67c 100644 --- a/src/ui/public/agg_types/__tests__/param_types/_field.js +++ b/src/ui/public/agg_types/__tests__/param_types/_field.js @@ -1,18 +1,22 @@ -import _ from 'lodash'; import expect from 'expect.js'; +import { reject } from 'lodash'; import ngMock from 'ng_mock'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; import AggTypesParamTypesFieldProvider from 'ui/agg_types/param_types/field'; + describe('Field', function () { let BaseAggParam; let FieldAggParam; + let indexPattern; beforeEach(ngMock.module('kibana')); // fetch out deps beforeEach(ngMock.inject(function (Private) { BaseAggParam = Private(AggTypesParamTypesBaseProvider); FieldAggParam = Private(AggTypesParamTypesFieldProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); describe('constructor', function () { @@ -24,4 +28,34 @@ describe('Field', function () { expect(aggParam).to.be.a(BaseAggParam); }); }); + + describe('getFieldOptions', function () { + it('should return only aggregatable fields by default', function () { + const aggParam = new FieldAggParam({ + name: 'field' + }); + + const fields = aggParam.getFieldOptions({ + getIndexPattern: () => indexPattern + }); + expect(fields).to.not.have.length(0); + for (const field of fields) { + expect(field.aggregatable).to.be(true); + } + }); + + it('should return all fields if onlyAggregatable is false', function () { + const aggParam = new FieldAggParam({ + name: 'field' + }); + + aggParam.onlyAggregatable = false; + + const fields = aggParam.getFieldOptions({ + getIndexPattern: () => indexPattern + }); + const nonAggregatableFields = reject(fields, 'aggregatable'); + expect(nonAggregatableFields).to.not.be.empty(); + }); + }); }); diff --git a/src/ui/public/agg_types/buckets/terms.js b/src/ui/public/agg_types/buckets/terms.js index 30e66f8630e43d..886bf183d4fe33 100644 --- a/src/ui/public/agg_types/buckets/terms.js +++ b/src/ui/public/agg_types/buckets/terms.js @@ -16,12 +16,13 @@ export default function TermsAggDefinition(Private) { const createFilter = Private(AggTypesBucketsCreateFilterTermsProvider); const routeBasedNotifier = Private(routeBasedNotifierProvider); + const aggFilter = ['!top_hits', '!percentiles', '!median', '!std_dev']; const orderAggSchema = (new Schemas([ { group: 'none', name: 'orderAgg', title: 'Order Agg', - aggFilter: ['!percentiles', '!median', '!std_dev'] + aggFilter: aggFilter } ])).all[0]; @@ -94,9 +95,15 @@ export default function TermsAggDefinition(Private) { $scope.$watch('responseValueAggs', updateOrderAgg); $scope.$watch('agg.params.orderBy', updateOrderAgg); + // Returns true if the agg is not compatible with the terms bucket + $scope.rejectAgg = function (agg) { + // aggFilter elements all starts with a '!' + // so the index of agg.type.name in a filter is 1 if it is included + return Boolean(aggFilter.find((filter) => filter.indexOf(agg.type.name) === 1)); + }; + function updateOrderAgg() { const agg = $scope.agg; - const aggs = agg.vis.aggs; const params = agg.params; const orderBy = params.orderBy; const paramDef = agg.type.params.byName.orderAgg; @@ -105,7 +112,11 @@ export default function TermsAggDefinition(Private) { if (!orderBy && prevOrderBy === INIT) { // abort until we get the responseValueAggs if (!$scope.responseValueAggs) return; - params.orderBy = (_.first($scope.responseValueAggs) || { id: 'custom' }).id; + let respAgg = _($scope.responseValueAggs).filter((agg) => !$scope.rejectAgg(agg)).first(); + if (!respAgg) { + respAgg = { id: '_term' }; + } + params.orderBy = respAgg.id; return; } @@ -115,15 +126,10 @@ export default function TermsAggDefinition(Private) { // we aren't creating a custom aggConfig if (!orderBy || orderBy !== 'custom') { params.orderAgg = null; - - if (orderBy === '_term') { - params.orderBy = '_term'; - return; - } - // ensure that orderBy is set to a valid agg - if (!_.find($scope.responseValueAggs, { id: orderBy })) { - params.orderBy = null; + const respAgg = _($scope.responseValueAggs).filter((agg) => !$scope.rejectAgg(agg)).find({ id: orderBy }); + if (!respAgg) { + params.orderBy = '_term'; } return; } diff --git a/src/ui/public/agg_types/controls/field.html b/src/ui/public/agg_types/controls/field.html index 14d0b56d984674..5ef83e18a3219c 100644 --- a/src/ui/public/agg_types/controls/field.html +++ b/src/ui/public/agg_types/controls/field.html @@ -3,7 +3,7 @@ Field - Analyzed Field diff --git a/src/ui/public/agg_types/controls/order_agg.html b/src/ui/public/agg_types/controls/order_agg.html index 2b44fe77fbd745..a407660380939d 100644 --- a/src/ui/public/agg_types/controls/order_agg.html +++ b/src/ui/public/agg_types/controls/order_agg.html @@ -9,6 +9,7 @@ @@ -27,4 +28,4 @@ group-name="'metrics'"> - \ No newline at end of file + diff --git a/src/ui/public/agg_types/controls/top_aggregate_and_size.html b/src/ui/public/agg_types/controls/top_aggregate_and_size.html new file mode 100644 index 00000000000000..76e490ddc9970d --- /dev/null +++ b/src/ui/public/agg_types/controls/top_aggregate_and_size.html @@ -0,0 +1,37 @@ +
    +
    + + + +
    +
    + + + +
    +
    diff --git a/src/ui/public/agg_types/controls/top_sort.html b/src/ui/public/agg_types/controls/top_sort.html new file mode 100644 index 00000000000000..f55c265090576b --- /dev/null +++ b/src/ui/public/agg_types/controls/top_sort.html @@ -0,0 +1,29 @@ +
    + + + +
    + +
    + + + +
    diff --git a/src/ui/public/agg_types/index.js b/src/ui/public/agg_types/index.js index 5ad33dadc47916..4409b6a1fbda3c 100644 --- a/src/ui/public/agg_types/index.js +++ b/src/ui/public/agg_types/index.js @@ -6,6 +6,7 @@ import AggTypesMetricsSumProvider from 'ui/agg_types/metrics/sum'; import AggTypesMetricsMedianProvider from 'ui/agg_types/metrics/median'; import AggTypesMetricsMinProvider from 'ui/agg_types/metrics/min'; import AggTypesMetricsMaxProvider from 'ui/agg_types/metrics/max'; +import AggTypesMetricsTopHitProvider from 'ui/agg_types/metrics/top_hit'; import AggTypesMetricsStdDeviationProvider from 'ui/agg_types/metrics/std_deviation'; import AggTypesMetricsCardinalityProvider from 'ui/agg_types/metrics/cardinality'; import AggTypesMetricsPercentilesProvider from 'ui/agg_types/metrics/percentiles'; @@ -32,7 +33,8 @@ export default function AggTypeService(Private) { Private(AggTypesMetricsStdDeviationProvider), Private(AggTypesMetricsCardinalityProvider), Private(AggTypesMetricsPercentilesProvider), - Private(AggTypesMetricsPercentileRanksProvider) + Private(AggTypesMetricsPercentileRanksProvider), + Private(AggTypesMetricsTopHitProvider) ], buckets: [ Private(AggTypesBucketsDateHistogramProvider), diff --git a/src/ui/public/agg_types/metrics/top_hit.js b/src/ui/public/agg_types/metrics/top_hit.js new file mode 100644 index 00000000000000..94c88578378f15 --- /dev/null +++ b/src/ui/public/agg_types/metrics/top_hit.js @@ -0,0 +1,203 @@ +import _ from 'lodash'; +import MetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; +import topSortEditor from 'ui/agg_types/controls/top_sort.html'; +import aggregateAndSizeEditor from 'ui/agg_types/controls/top_aggregate_and_size.html'; + +export default function AggTypeMetricTopProvider(Private) { + const MetricAggType = Private(MetricAggTypeProvider); + const fieldFormats = Private(RegistryFieldFormatsProvider); + + const isNumber = function (type) { + return type === 'number'; + }; + + return new MetricAggType({ + name: 'top_hits', + title: 'Top Hit', + makeLabel: function (aggConfig) { + let prefix = aggConfig.params.sortOrder.val === 'desc' ? 'Last' : 'First'; + if (aggConfig.params.size !== 1) { + prefix += ` ${aggConfig.params.size}`; + } + return `${prefix} ${aggConfig.params.field.displayName}`; + }, + params: [ + { + name: 'field', + onlyAggregatable: false, + showAnalyzedWarning: false, + filterFieldTypes: function (vis, value) { + if (vis.type.name === 'table' || vis.type.name === 'metric') { + return true; + } + return value === 'number'; + }, + write(agg, output) { + const field = agg.params.field; + output.params = {}; + + if (field.scripted) { + output.params.script_fields = { + [ field.name ]: { + script: { + inline: field.script, + lang: field.lang + } + } + }; + } else { + if (field.doc_values) { + output.params.docvalue_fields = [ field.name ]; + } + output.params._source = field.name === '_source' ? true : field.name; + } + } + }, + { + name: 'aggregate', + type: 'optioned', + editor: aggregateAndSizeEditor, + options: [ + { + display: 'Min', + isCompatibleType: isNumber, + isCompatibleVis: _.constant(true), + disabled: true, + val: 'min' + }, + { + display: 'Max', + isCompatibleType: isNumber, + isCompatibleVis: _.constant(true), + disabled: true, + val: 'max' + }, + { + display: 'Sum', + isCompatibleType: isNumber, + isCompatibleVis: _.constant(true), + disabled: true, + val: 'sum' + }, + { + display: 'Average', + isCompatibleType: isNumber, + isCompatibleVis: _.constant(true), + disabled: true, + val: 'average' + }, + { + display: 'Concatenate', + isCompatibleType: _.constant(true), + isCompatibleVis: function (name) { + return name === 'metric' || name === 'table'; + }, + disabled: true, + val: 'concat' + } + ], + controller: function ($scope) { + $scope.options = []; + $scope.$watchGroup([ 'agg.vis.type.name', 'agg.params.field.type' ], function ([ visName, fieldType ]) { + if (fieldType && visName) { + $scope.options = _.filter($scope.aggParam.options, option => { + return option.isCompatibleVis(visName) && option.isCompatibleType(fieldType); + }); + if ($scope.options.length === 1) { + $scope.agg.params.aggregate = $scope.options[0]; + } + } + }); + }, + write: _.noop + }, + { + name: 'size', + editor: null, // size setting is done together with the aggregation setting + default: 1 + }, + { + name: 'sortField', + type: 'field', + editor: null, + filterFieldTypes: [ 'number', 'date', 'ip', 'string' ], + default: function (agg) { + return agg.vis.indexPattern.timeFieldName; + }, + write: _.noop // prevent default write, it is handled below + }, + { + name: 'sortOrder', + type: 'optioned', + default: 'desc', + editor: topSortEditor, + options: [ + { display: 'Descending', val: 'desc' }, + { display: 'Ascending', val: 'asc' } + ], + write(agg, output) { + const sortField = agg.params.sortField; + const sortOrder = agg.params.sortOrder; + + if (sortField.scripted) { + output.params.sort = [ + { + _script: { + script: { + inline: sortField.script, + lang: sortField.lang + }, + type: sortField.type, + order: sortOrder.val + } + } + ]; + } else { + output.params.sort = [ + { + [ sortField.name ]: { + order: sortOrder.val + } + } + ]; + } + } + } + ], + getValue(agg, bucket) { + const hits = _.get(bucket, `${agg.id}.hits.hits`); + if (!hits || !hits.length) { + return null; + } + const path = agg.params.field.name; + + let values = _(hits).map(hit => { + return path === '_source' ? hit._source : agg.vis.indexPattern.flattenHit(hit, true)[path]; + }) + .flatten() + .value(); + + if (values.length === 1) { + values = values[0]; + } + + if (_.isArray(values)) { + if (!_.compact(values).length) { + return null; + } + switch (agg.params.aggregate.val) { + case 'max': + return _.max(values); + case 'min': + return _.min(values); + case 'sum': + return _.sum(values); + case 'average': + return _.sum(values) / values.length; + } + } + return values; + } + }); +} diff --git a/src/ui/public/agg_types/param_types/field.js b/src/ui/public/agg_types/param_types/field.js index 4919b37caaec3b..798b857a7ceeeb 100644 --- a/src/ui/public/agg_types/param_types/field.js +++ b/src/ui/public/agg_types/param_types/field.js @@ -18,6 +18,10 @@ export default function FieldAggParamFactory(Private, $filter) { FieldAggParam.prototype.editor = editorHtml; FieldAggParam.prototype.scriptable = true; FieldAggParam.prototype.filterFieldTypes = '*'; + // retain only the fields with the aggregatable property if the onlyAggregatable option is true + FieldAggParam.prototype.onlyAggregatable = true; + // show a warning about the field being analyzed + FieldAggParam.prototype.showAnalyzedWarning = true; /** * Called to serialize values for saving an aggConfig object @@ -36,14 +40,20 @@ export default function FieldAggParamFactory(Private, $filter) { const indexPattern = aggConfig.getIndexPattern(); let fields = indexPattern.fields.raw; - fields = fields.filter(f => f.aggregatable); + if (this.onlyAggregatable) { + fields = fields.filter(f => f.aggregatable); + } if (!this.scriptable) { fields = fields.filter(field => !field.scripted); } if (this.filterFieldTypes) { - fields = $filter('fieldType')(fields, this.filterFieldTypes); + let filters = this.filterFieldTypes; + if (_.isFunction(this.filterFieldTypes)) { + filters = this.filterFieldTypes.bind(this, aggConfig.vis); + } + fields = $filter('fieldType')(fields, filters); fields = $filter('orderBy')(fields, ['type', 'name']); } diff --git a/src/ui/public/autoload/styles.js b/src/ui/public/autoload/styles.js index e1329e49ffb99f..929e26ba69b657 100644 --- a/src/ui/public/autoload/styles.js +++ b/src/ui/public/autoload/styles.js @@ -1,5 +1,5 @@ // Kibana UI Framework -require('../../../ui_framework/components/index.scss'); +require('../../../../ui_framework/dist/ui_framework.css'); // All Kibana styles inside of the /styles dir const context = require.context('../styles', false, /[\/\\](?!mixins|variables|_|\.)[^\/\\]+\.less/); diff --git a/src/ui/public/chrome/config/filter.html b/src/ui/public/chrome/config/filter.html index 71a3dd32645ef3..334afb4c0089f3 100644 --- a/src/ui/public/chrome/config/filter.html +++ b/src/ui/public/chrome/config/filter.html @@ -3,5 +3,7 @@ to="timefilter.time.to" mode="timefilter.time.mode" active-tab="'filter'" - interval="timefilter.refreshInterval"> + interval="timefilter.refreshInterval" + on-filter-select="updateFilter(from, to)" + on-interval-select="updateInterval(interval)"> diff --git a/src/ui/public/chrome/config/interval.html b/src/ui/public/chrome/config/interval.html index d41a6017097092..d898a397cb5f91 100644 --- a/src/ui/public/chrome/config/interval.html +++ b/src/ui/public/chrome/config/interval.html @@ -3,5 +3,7 @@ to="timefilter.time.to" mode="timefilter.time.mode" active-tab="'interval'" - interval="timefilter.refreshInterval"> + interval="timefilter.refreshInterval" + on-filter-select="updateFilter(from, to)" + on-interval-select="updateInterval(interval)"> diff --git a/src/ui/public/courier/saved_object/saved_object_loader.js b/src/ui/public/courier/saved_object/saved_object_loader.js index d4ad71729847a2..fee5279a362584 100644 --- a/src/ui/public/courier/saved_object/saved_object_loader.js +++ b/src/ui/public/courier/saved_object/saved_object_loader.js @@ -78,11 +78,12 @@ export class SavedObjectLoader { */ find(searchString, size = 100) { let body; + if (searchString) { body = { query: { - simple_query_string: { - query: searchString + '*', + query_string: { + query: /^\w+$/.test(searchString) ? `${searchString}*` : searchString, fields: ['title^3', 'description'], default_operator: 'AND' } @@ -94,10 +95,23 @@ export class SavedObjectLoader { return this.esAdmin.search({ index: this.kbnIndex, - type: this.type.toLowerCase(), + type: this.lowercaseType, body, size }) + .catch(err => { + // attempt to mimic simple_query_string, swallow formatting error + if (err.statusCode === 400) { + return { + hits: { + total: 0, + hits: [], + } + }; + } + + throw err; + }) .then((resp) => { return { total: resp.hits.total, diff --git a/src/ui/public/directives/__tests__/timepicker.js b/src/ui/public/directives/__tests__/timepicker.js index 6314cbe1cf5ab6..71ff2146ef2e63 100644 --- a/src/ui/public/directives/__tests__/timepicker.js +++ b/src/ui/public/directives/__tests__/timepicker.js @@ -46,6 +46,8 @@ const init = function () { } }; $parentScope.timefilter = timefilter; + $parentScope.updateInterval = sinon.spy(); + $parentScope.updateFilter = sinon.spy(); // Create the element $elem = angular.element( @@ -54,7 +56,9 @@ const init = function () { ' to="timefilter.time.to"' + ' mode="timefilter.time.mode"' + ' active-tab="timefilter.timepickerActiveTab"' + - ' interval="timefilter.refreshInterval">' + + ' interval="timefilter.refreshInterval"' + + ' on-interval-select="updateInterval(interval)"' + + ' on-filter-select="updateFilter(from, to)">' + '' ); @@ -99,64 +103,34 @@ describe('timepicker directive', function () { done(); }); - it('should have a $scope.setRefreshInterval() that sets interval variable', function (done) { + it('should have a $scope.setRefreshInterval() that calls handler', function (done) { $scope.setRefreshInterval({ value : 10000 }); - expect($scope.interval).to.have.property('value', 10000); - done(); - }); - - it('should set the interval on the courier', function (done) { - // Change refresh interval and digest - $scope.setRefreshInterval({ value : 1000 }); - $elem.scope().$digest(); - expect($courier.searchLooper.loopInterval()).to.be(1000); - done(); - }); - - it('should disable the looper when paused', function (done) { - $scope.setRefreshInterval({ value : 1000, pause: true }); - $elem.scope().$digest(); - expect($courier.searchLooper.loopInterval()).to.be(0); - expect($scope.interval.value).to.be(1000); - done(); - }); - - it('but keep interval.value set', function (done) { - $scope.setRefreshInterval({ value : 1000, pause: true }); - $elem.scope().$digest(); - expect($scope.interval.value).to.be(1000); + sinon.assert.calledOnce($parentScope.updateInterval); + expect($parentScope.updateInterval.firstCall.args[0]).to.have.property('value', 10000); done(); }); it('should unpause when setRefreshInterval is called without pause:true', function (done) { $scope.setRefreshInterval({ value : 1000, pause: true }); - expect($scope.interval.pause).to.be(true); + expect($parentScope.updateInterval.getCall(0).args[0]).to.have.property('pause', true); $scope.setRefreshInterval({ value : 1000, pause: false }); - expect($scope.interval.pause).to.be(false); + expect($parentScope.updateInterval.getCall(1).args[0]).to.have.property('pause', false); $scope.setRefreshInterval({ value : 1000 }); - expect($scope.interval.pause).to.be(false); + expect($parentScope.updateInterval.getCall(2).args[0]).to.have.property('pause', false); done(); }); it('should highlight the current active interval', function (done) { - $scope.setRefreshInterval({ value: 300000 }); + $scope.interval = { value: 300000 }; $elem.scope().$digest(); expect($elem.find('.refresh-interval-active').length).to.be(1); expect($elem.find('.refresh-interval-active').text().trim()).to.be('5 minutes'); done(); }); - - it('should default the interval on the courier with incorrect values', function (done) { - // Change refresh interval and digest - $scope.setRefreshInterval(); - $elem.scope().$digest(); - expect($courier.searchLooper.loopInterval()).to.be(0); - done(); - }); }); describe('mode setting', function () { @@ -198,10 +172,11 @@ describe('timepicker directive', function () { done(); }); - it('should have a $scope.setQuick() that sets the to and from variables to strings', function (done) { + it('should have a $scope.setQuick() that calls handler', function (done) { $scope.setQuick('now', 'now'); - expect($scope.from).to.be('now'); - expect($scope.to).to.be('now'); + sinon.assert.calledOnce($parentScope.updateFilter); + expect($parentScope.updateFilter.firstCall.args[0]).to.be('now'); + expect($parentScope.updateFilter.firstCall.args[1]).to.be('now'); done(); }); }); @@ -312,24 +287,25 @@ describe('timepicker directive', function () { $scope.relative.count = 1; $scope.relative.unit = 's'; $scope.applyRelative(); - expect($scope.from).to.be('now-1s'); + sinon.assert.calledOnce($parentScope.updateFilter); + expect($parentScope.updateFilter.getCall(0).args[0]).to.be('now-1s'); $scope.relative.count = 2; $scope.relative.unit = 'm'; $scope.applyRelative(); - expect($scope.from).to.be('now-2m'); + expect($parentScope.updateFilter.getCall(1).args[0]).to.be('now-2m'); $scope.relative.count = 3; $scope.relative.unit = 'h'; $scope.applyRelative(); - expect($scope.from).to.be('now-3h'); + expect($parentScope.updateFilter.getCall(2).args[0]).to.be('now-3h'); // Enable rounding $scope.relative.round = true; $scope.relative.count = 7; $scope.relative.unit = 'd'; $scope.applyRelative(); - expect($scope.from).to.be('now-7d/d'); + expect($parentScope.updateFilter.getCall(3).args[0]).to.be('now-7d/d'); done(); }); @@ -398,16 +374,6 @@ describe('timepicker directive', function () { done(); }); - it('should parse the time of scope.from and scope.to to set its own variables', function (done) { - $scope.setQuick('now-30m', 'now'); - $scope.setMode('absolute'); - $scope.$digest(); - - expect($scope.absolute.from.valueOf()).to.be(moment().subtract(30, 'minutes').valueOf()); - expect($scope.absolute.to.valueOf()).to.be(moment().valueOf()); - done(); - }); - it('should update its own variables if timefilter time is updated', function (done) { $scope.setMode('absolute'); $scope.$digest(); @@ -438,9 +404,8 @@ describe('timepicker directive', function () { }); it('should only copy its input to scope.from and scope.to when scope.applyAbsolute() is called', function (done) { - $scope.setQuick('now-30m', 'now'); - expect($scope.from).to.be('now-30m'); - expect($scope.to).to.be('now'); + $scope.from = 'now-30m'; + $scope.to = 'now'; $scope.absolute.from = moment('2012-02-01'); $scope.absolute.to = moment('2012-02-11'); @@ -448,8 +413,8 @@ describe('timepicker directive', function () { expect($scope.to).to.be('now'); $scope.applyAbsolute(); - expect($scope.from.valueOf()).to.be(moment('2012-02-01').valueOf()); - expect($scope.to.valueOf()).to.be(moment('2012-02-11').valueOf()); + expect($parentScope.updateFilter.firstCall.args[0]).to.eql(moment('2012-02-01')); + expect($parentScope.updateFilter.firstCall.args[1]).to.eql(moment('2012-02-11')); $scope.$digest(); diff --git a/src/ui/public/directives/saved_object_finder.js b/src/ui/public/directives/saved_object_finder.js index 4d80aab07efcb6..99c0a30db67369 100644 --- a/src/ui/public/directives/saved_object_finder.js +++ b/src/ui/public/directives/saved_object_finder.js @@ -20,7 +20,12 @@ module.directive('savedObjectFinder', function ($location, $injector, kbnUrl, Pr // optional on-choose attr, sets the userOnChoose in our scope userOnChoose: '=?onChoose', // optional useLocalManagement attr, removes link to management section - useLocalManagement: '=?useLocalManagement' + useLocalManagement: '=?useLocalManagement', + /** + * @type {function} - an optional function. If supplied an `Add new X` button is shown + * and this function is called when clicked. + */ + onAddNew: '=' }, template: savedObjectFinderTemplate, controllerAs: 'finder', diff --git a/src/ui/public/factories/__tests__/events.js b/src/ui/public/factories/__tests__/events.js index 974f07b9221481..1b4a6bc3d7d61d 100644 --- a/src/ui/public/factories/__tests__/events.js +++ b/src/ui/public/factories/__tests__/events.js @@ -89,7 +89,7 @@ describe('Events', function () { }); }); - it('should handle mulitple identical emits in the same tick', function () { + it('should handle multiple identical emits in the same tick', function () { const obj = new Events(); const handler1 = sinon.stub(); diff --git a/src/ui/public/filter_manager/filter_manager.js b/src/ui/public/filter_manager/filter_manager.js index dd5fa6bee8bea4..5930629c15f401 100644 --- a/src/ui/public/filter_manager/filter_manager.js +++ b/src/ui/public/filter_manager/filter_manager.js @@ -25,7 +25,7 @@ export default function (Private) { return filter.exists.field === value; } - if (_.get(filter, 'query.match')) { + if (_.has(filter, 'query.match')) { return filter.query.match[fieldName] && filter.query.match[fieldName].query === value; } diff --git a/src/ui/public/filter_manager/lib/phrase.js b/src/ui/public/filter_manager/lib/phrase.js index 16c9c89d17e97d..748e2ce976814c 100644 --- a/src/ui/public/filter_manager/lib/phrase.js +++ b/src/ui/public/filter_manager/lib/phrase.js @@ -4,14 +4,14 @@ export default function buildPhraseFilter(field, value, indexPattern) { if (field.scripted) { // See https://github.com/elastic/elasticsearch/issues/20941 and https://github.com/elastic/kibana/issues/8677 - // for the reason behind this change. ES doesn't handle boolean types very well, so they come - // back as strings. + // and https://github.com/elastic/elasticsearch/pull/22201 + // for the reason behind this change. Aggs now return boolean buckets with a key of 1 or 0. let convertedValue = value; if (typeof value !== 'boolean' && field.type === 'boolean') { - if (value !== 'true' && value !== 'false') { + if (value !== 1 && value !== 0) { throw new Error('Boolean scripted fields must return true or false'); } - convertedValue = value === 'true' ? true : false; + convertedValue = value === 1 ? true : false; } const script = buildInlineScriptForPhraseFilter(field); diff --git a/src/ui/public/filters/__tests__/prop_filter.js b/src/ui/public/filters/__tests__/prop_filter.js new file mode 100644 index 00000000000000..a9c6cdc57123db --- /dev/null +++ b/src/ui/public/filters/__tests__/prop_filter.js @@ -0,0 +1,58 @@ +import expect from 'expect.js'; +import propFilter from 'ui/filters/_prop_filter'; + +describe('prop filter', function () { + let nameFilter; + + beforeEach(function () { + nameFilter = propFilter('name'); + }); + + function getObjects(...names) { + const count = new Map(); + const objects = []; + + for (const name of names) { + if (!count.has(name)) { + count.set(name, 1); + } + objects.push({ + name: name, + title: `${name} ${count.get(name)}` + }); + count.set(name, count.get(name) + 1); + } + return objects; + } + + it('should keep only the tables', function () { + const objects = getObjects('table', 'table', 'pie'); + expect(nameFilter(objects, 'table')).to.eql(getObjects('table', 'table')); + }); + + it('should support comma-separated values', function () { + const objects = getObjects('table', 'line', 'pie'); + expect(nameFilter(objects, 'table,line')).to.eql(getObjects('table', 'line')); + }); + + it('should support an array of values', function () { + const objects = getObjects('table', 'line', 'pie'); + expect(nameFilter(objects, [ 'table', 'line' ])).to.eql(getObjects('table', 'line')); + }); + + it('should return all objects', function () { + const objects = getObjects('table', 'line', 'pie'); + expect(nameFilter(objects, '*')).to.eql(objects); + }); + + it('should allow negation', function () { + const objects = getObjects('table', 'line', 'pie'); + expect(nameFilter(objects, [ '!line' ])).to.eql(getObjects('table', 'pie')); + }); + + it('should support a function for specifying what should be kept', function () { + const objects = getObjects('table', 'line', 'pie'); + const line = (value) => value === 'line'; + expect(nameFilter(objects, line)).to.eql(getObjects('line')); + }); +}); diff --git a/src/ui/public/filters/_prop_filter.js b/src/ui/public/filters/_prop_filter.js index f2893127edf9bf..cd007e8ad20b49 100644 --- a/src/ui/public/filters/_prop_filter.js +++ b/src/ui/public/filters/_prop_filter.js @@ -13,13 +13,18 @@ function propFilter(prop) { * must contain * * @param {array} list - array of items to filter - * @param {array|string} filters - the values to match against the list. Can be - * an array, a single value as a string, or a comma - * -seperated list of items + * @param {function|array|string} filters - the values to match against the list + * - if a function, it is expected to take the field property as argument and returns true to keep it. + * - Can be also an array, a single value as a string, or a comma-seperated list of items * @return {array} - the filtered list */ return function (list, filters) { if (!filters) return filters; + + if (_.isFunction(filters)) { + return list.filter((item) => filters(item[prop])); + } + if (!_.isArray(filters)) filters = filters.split(','); if (_.contains(filters, '*')) return list; diff --git a/src/ui/public/fixed_scroll.js b/src/ui/public/fixed_scroll.js index 09963b82870c04..0eda343b37e51b 100644 --- a/src/ui/public/fixed_scroll.js +++ b/src/ui/public/fixed_scroll.js @@ -117,11 +117,13 @@ uiModules } } - $scope.$watch(debounce(checkWidth, 100)); + const debouncedCheckWidth = debounce(checkWidth, 100); + $scope.$watch(debouncedCheckWidth); // cleanup when the scope is destroyed $scope.$on('$destroy', function () { cleanUp(); + debouncedCheckWidth.cancel(); $scroller = $window = null; }); } diff --git a/src/ui/public/index_patterns/__tests__/flatten_hit.js b/src/ui/public/index_patterns/__tests__/flatten_hit.js index 882d44fafa6554..cc23358932062d 100644 --- a/src/ui/public/index_patterns/__tests__/flatten_hit.js +++ b/src/ui/public/index_patterns/__tests__/flatten_hit.js @@ -4,18 +4,17 @@ import ngMock from 'ng_mock'; import IndexPatternsFlattenHitProvider from 'ui/index_patterns/_flatten_hit'; describe('IndexPattern#flattenHit()', function () { - - let flattenHit; let config; let hit; - let flat; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { const indexPattern = { fields: { byName: { + 'tags.text': { type: 'string' }, + 'tags.label': { type: 'string' }, 'message': { type: 'string' }, 'geo.coordinates': { type: 'geo_point' }, 'geo.dest': { type: 'string' }, @@ -33,7 +32,12 @@ describe('IndexPattern#flattenHit()', function () { } }; - flattenHit = Private(IndexPatternsFlattenHitProvider)(indexPattern).uncached; + const cachedFlatten = Private(IndexPatternsFlattenHitProvider)(indexPattern); + flattenHit = function (hit, deep = false) { + delete hit.$$_flattened; + return cachedFlatten(hit, deep); + }; + config = $injector.get('config'); hit = { @@ -46,7 +50,10 @@ describe('IndexPattern#flattenHit()', function () { }, bytes: 10039103, '@timestamp': (new Date()).toString(), - tags: [{ text: 'foo' }, { text: 'bar' }], + tags: [ + { text: 'foo', label: [ 'FOO1', 'FOO2' ] }, + { text: 'bar', label: 'BAR' } + ], groups: ['loners'], noMapping: true, team: [ @@ -61,11 +68,11 @@ describe('IndexPattern#flattenHit()', function () { random: [0.12345] } }; - - flat = flattenHit(hit); })); it('flattens keys as far down as the mapping goes', function () { + const flat = flattenHit(hit); + expect(flat).to.have.property('geo.coordinates', hit._source.geo.coordinates); expect(flat).to.not.have.property('geo.coordinates.lat'); expect(flat).to.not.have.property('geo.coordinates.lon'); @@ -77,22 +84,42 @@ describe('IndexPattern#flattenHit()', function () { }); it('flattens keys not in the mapping', function () { + const flat = flattenHit(hit); + expect(flat).to.have.property('noMapping', true); expect(flat).to.have.property('groups'); expect(flat.groups).to.eql(['loners']); }); it('flattens conflicting types in the mapping', function () { + const flat = flattenHit(hit); + expect(flat).to.not.have.property('user'); expect(flat).to.have.property('user.name', hit._source.user.name); expect(flat).to.have.property('user.id', hit._source.user.id); }); - it('preserves objects in arrays', function () { + it('should preserve objects in arrays if deep argument is false', function () { + const flat = flattenHit(hit); + expect(flat).to.have.property('tags', hit._source.tags); }); + it('should expand objects in arrays if deep argument is true', function () { + const flat = flattenHit(hit, true); + + expect(flat['tags.text']).to.be.eql([ 'foo', 'bar' ]); + }); + + it('should support arrays when expanding objects in arrays if deep argument is true', function () { + const flat = flattenHit(hit, true); + + expect(flat['tags.label']).to.be.eql([ 'FOO1', 'FOO2', 'BAR' ]); + }); + it('does not enter into nested fields', function () { + const flat = flattenHit(hit); + expect(flat).to.have.property('team', hit._source.team); expect(flat).to.not.have.property('team.name'); expect(flat).to.not.have.property('team.role'); @@ -101,24 +128,28 @@ describe('IndexPattern#flattenHit()', function () { }); it('unwraps script fields', function () { + const flat = flattenHit(hit); + expect(flat).to.have.property('delta', 42); }); it('assumes that all fields are "computed fields"', function () { + const flat = flattenHit(hit); + expect(flat).to.have.property('random', 0.12345); }); it('ignores fields that start with an _ and are not in the metaFields', function () { config.set('metaFields', ['_metaKey']); hit.fields._notMetaKey = [100]; - flat = flattenHit(hit); + const flat = flattenHit(hit); expect(flat).to.not.have.property('_notMetaKey'); }); it('includes underscore-prefixed keys that are in the metaFields', function () { config.set('metaFields', ['_metaKey']); hit.fields._metaKey = [100]; - flat = flattenHit(hit); + const flat = flattenHit(hit); expect(flat).to.have.property('_metaKey', 100); }); @@ -126,7 +157,7 @@ describe('IndexPattern#flattenHit()', function () { hit.fields._metaKey = [100]; config.set('metaFields', ['_metaKey']); - flat = flattenHit(hit); + let flat = flattenHit(hit); expect(flat).to.have.property('_metaKey', 100); config.set('metaFields', []); @@ -137,7 +168,7 @@ describe('IndexPattern#flattenHit()', function () { it('handles fields that are not arrays, like _timestamp', function () { hit.fields._metaKey = 20000; config.set('metaFields', ['_metaKey']); - flat = flattenHit(hit); + const flat = flattenHit(hit); expect(flat).to.have.property('_metaKey', 20000); }); }); diff --git a/src/ui/public/index_patterns/_flatten_hit.js b/src/ui/public/index_patterns/_flatten_hit.js index 818eb2eb95a7cb..87eff457547f9e 100644 --- a/src/ui/public/index_patterns/_flatten_hit.js +++ b/src/ui/public/index_patterns/_flatten_hit.js @@ -1,4 +1,5 @@ import _ from 'lodash'; + // Takes a hit, merges it with any stored/scripted fields, and with the metaFields // returns a flattened version export default function FlattenHitProvider(config) { @@ -8,7 +9,7 @@ export default function FlattenHitProvider(config) { metaFields = value; }); - function flattenHit(indexPattern, hit) { + function flattenHit(indexPattern, hit, deep) { const flat = {}; // recursively merge _source @@ -18,13 +19,28 @@ export default function FlattenHitProvider(config) { _.forOwn(obj, function (val, key) { key = keyPrefix + key; - if (flat[key] !== void 0) return; + if (deep) { + const isNestedField = fields[key] && fields[key].type === 'nested'; + const isArrayOfObjects = _.isArray(val) && _.isPlainObject(_.first(val)); + if (isArrayOfObjects && !isNestedField) { + _.each(val, v => flatten(v, key)); + return; + } + } else if (flat[key] !== void 0) { + return; + } - const hasValidMapping = (fields[key] && fields[key].type !== 'conflict'); + const hasValidMapping = fields[key] && fields[key].type !== 'conflict'; const isValue = !_.isPlainObject(val); if (hasValidMapping || isValue) { - flat[key] = val; + if (!flat[key]) { + flat[key] = val; + } else if (_.isArray(flat[key])) { + flat[key].push(val); + } else { + flat[key] = [ flat[key], val ]; + } return; } @@ -48,13 +64,8 @@ export default function FlattenHitProvider(config) { } return function flattenHitWrapper(indexPattern) { - function cachedFlatten(hit) { - return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit)); - } - - cachedFlatten.uncached = _.partial(flattenHit, indexPattern); - - return cachedFlatten; + return function cachedFlatten(hit, deep = false) { + return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit, deep)); + }; }; } - diff --git a/src/ui/public/partials/bread_crumbs.html b/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.html similarity index 100% rename from src/ui/public/partials/bread_crumbs.html rename to src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.html diff --git a/src/ui/public/directives/bread_crumbs.js b/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js similarity index 90% rename from src/ui/public/directives/bread_crumbs.js rename to src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js index 612d69c4da4023..3617c519a81a4d 100644 --- a/src/ui/public/directives/bread_crumbs.js +++ b/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import chrome from 'ui/chrome/chrome'; -import breadCrumbsTemplate from 'ui/partials/bread_crumbs.html'; +import breadCrumbsTemplate from './bread_crumbs.html'; import uiModules from 'ui/modules'; const module = uiModules.get('kibana'); diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav.js b/src/ui/public/kbn_top_nav/kbn_top_nav.js index 8d3174f2ae3541..2d4cd3ef914764 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/ui/public/kbn_top_nav/kbn_top_nav.js @@ -1,3 +1,20 @@ +/** + * A configuration object for a top nav component. + * @typedef {Object} KbnTopNavConfig + * @type Object + * @property {string} key - A display string which will be shown in the top nav for this option. + * @property {string} [description] - optional, used for the screen-reader description of this + * menu. Defaults to "Toggle ${key} view" for templated menu items and just "${key}" for + * programmatic menu items + * @property {string} testId - for testing purposes, can be used to retrieve this item. + * @property {Object} [template] - an html template that will be shown when this item is clicked. + * If template is not given then run should be supplied. + * @property {function} [run] - an optional function that will be run when the nav item is clicked. + * Either this or template parameter should be specified. + * @param {boolean} [hideButton] - optional, set to true to prevent a menu item from being created. + * This allow injecting templates into the navbar that don't have an associated template + */ + /** * kbnTopNav directive * @@ -9,27 +26,11 @@ * * Menu items/templates are passed to the kbnTopNav via the config attribute * and should be defined as an array of objects. Each object represents a menu - * item and should have the following properties: + * item and should be of type kbnTopNavConfig. * - * @param {Array|KbnTopNavController} config - * @param {string} config[].key - * - the uniq key for this menu item. - * @param {string} [config[].label] - * - optional, string that will be displayed for the menu button. - * Defaults to the key - * @param {string} [config[].description] - * - optional, used for the screen-reader description of this menu - * item, defaults to "Toggle ${key} view" for templated menu items - * and just "${key}" for programatic menu items - * @param {boolean} [config[].hideButton] - * - optional, set to true to prevent a menu item from being created. - * This allow injecting templates into the navbar that don't have - * an associated template - * @param {function} [config[].run] - * - optional, function to call when the menu item is clicked, defaults - * to toggling the template + * @param {Array|KbnTopNavController} config * - * Programatic control of the navbar can be acheived one of two ways + * Programmatic control of the navbar can be achieved one of two ways */ import _ from 'lodash'; @@ -40,6 +41,7 @@ import uiModules from 'ui/modules'; import template from './kbn_top_nav.html'; import KbnTopNavControllerProvider from './kbn_top_nav_controller'; import RegistryNavbarExtensionsProvider from 'ui/registry/navbar_extensions'; +import './bread_crumbs/bread_crumbs'; const module = uiModules.get('kibana'); @@ -98,6 +100,7 @@ module.directive('kbnTopNav', function (Private) { }); const extensions = getNavbarExtensions($attrs.name); + let controls = _.get($scope, $attrs.config, []); if (controls instanceof KbnTopNavController) { controls.addItems(extensions); diff --git a/src/ui/public/paginated_table/__tests__/index.js b/src/ui/public/paginated_table/__tests__/index.js index 08057156b65a44..fcf9a6593c6738 100644 --- a/src/ui/public/paginated_table/__tests__/index.js +++ b/src/ui/public/paginated_table/__tests__/index.js @@ -47,13 +47,21 @@ describe('paginated table', function () { }; }; - const renderTable = function (cols, rows, perPage, sort) { + const renderTable = function (cols, rows, perPage, sort, showBlankRows) { $scope.cols = cols || []; $scope.rows = rows || []; $scope.perPage = perPage || defaultPerPage; $scope.sort = sort || {}; + $scope.showBlankRows = showBlankRows; - $el = $compile('')($scope); + const template = ` + `; + $el = $compile(template)($scope); $scope.$digest(); }; @@ -107,6 +115,18 @@ describe('paginated table', function () { // add 2 for the first and last page links expect($el.find('paginate-controls a').size()).to.be(pageCount + 2); }); + + it('should not show blank rows on last page when so specified', function () { + const rowCount = 7; + const perPageCount = 10; + const data = makeData(3, rowCount); + const pageCount = Math.ceil(rowCount / perPageCount); + + renderTable(data.columns, data.rows, perPageCount, null, false); + const tableRows = $el.find('tbody tr'); + expect(tableRows.size()).to.be(rowCount); + }); + }); describe('sorting', function () { diff --git a/src/ui/public/paginated_table/paginated_table.html b/src/ui/public/paginated_table/paginated_table.html index 1d64e70180ce68..0c2808a5531321 100644 --- a/src/ui/public/paginated_table/paginated_table.html +++ b/src/ui/public/paginated_table/paginated_table.html @@ -26,7 +26,7 @@ - + diff --git a/src/ui/public/paginated_table/paginated_table.js b/src/ui/public/paginated_table/paginated_table.js index 1fbef21b8479bf..9ea11bd714fd49 100644 --- a/src/ui/public/paginated_table/paginated_table.js +++ b/src/ui/public/paginated_table/paginated_table.js @@ -15,6 +15,7 @@ uiModules rows: '=', columns: '=', perPage: '=?', + showBlankRows: '=?', sortHandler: '=?', sort: '=?', showSelector: '=?', @@ -51,6 +52,14 @@ uiModules } }; + self.rowsToShow = function (numRowsPerPage, actualNumRowsOnThisPage) { + if ($scope.showBlankRows === false) { + return actualNumRowsOnThisPage; + } else { + return numRowsPerPage; + } + }; + function valueGetter(row) { let value = row[self.sort.columnIndex]; if (value && value.value != null) value = value.value; diff --git a/src/ui/public/partials/saved_object_finder.html b/src/ui/public/partials/saved_object_finder.html index 4075549f990cd8..809f7ffb89061a 100644 --- a/src/ui/public/partials/saved_object_finder.html +++ b/src/ui/public/partials/saved_object_finder.html @@ -18,6 +18,11 @@
    {{finder.hitCount}} of {{finder.hitCount}}
    + -
    + + + + +
    +
    Limit to
    + +
    pages
    +
    + diff --git a/ui_framework/doc_site/src/views/bar/bar_example.jsx b/ui_framework/doc_site/src/views/bar/bar_example.jsx new file mode 100644 index 00000000000000..1b9663e97f3588 --- /dev/null +++ b/ui_framework/doc_site/src/views/bar/bar_example.jsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { + createExample, +} from '../../services'; + +export default createExample([{ + title: 'Bar', + description: ( +
    +

    Use the Bar to organize controls in a horizontal layout. This is especially useful for surfacing controls in the corners of a view.

    +

    Note: Instead of using this component with a Table, try using the ControlledTable, ToolBar, and ToolBarFooter components.

    +
    + ), + html: require('./bar.html'), + hasDarkTheme: false, +}, { + title: 'One section', + description: ( +

    A Bar with one section will align it to the right, by default. To align it to the left, just add another section and leave it empty, or don't use a Bar at all.

    + ), + html: require('./bar_one_section.html'), + hasDarkTheme: false, +}, { + title: 'Three sections', + description: ( +

    Technically the Bar can contain three or more sections, but there's no established use-case for this.

    + ), + html: require('./bar_three_sections.html'), + hasDarkTheme: false, +}]); diff --git a/ui_framework/doc_site/src/views/bar/bar_one_section.html b/ui_framework/doc_site/src/views/bar/bar_one_section.html new file mode 100644 index 00000000000000..120421a1138c39 --- /dev/null +++ b/ui_framework/doc_site/src/views/bar/bar_one_section.html @@ -0,0 +1,12 @@ +
    +
    +
    + + +
    +
    +
    diff --git a/ui_framework/doc_site/src/views/bar/bar_three_sections.html b/ui_framework/doc_site/src/views/bar/bar_three_sections.html new file mode 100644 index 00000000000000..f9941351a11327 --- /dev/null +++ b/ui_framework/doc_site/src/views/bar/bar_three_sections.html @@ -0,0 +1,38 @@ +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    +
    Limit to
    + +
    pages
    + +
    + + +
    +
    +
    diff --git a/src/ui_framework/doc_site/src/views/button/button_basic.html b/ui_framework/doc_site/src/views/button/button_basic.html similarity index 100% rename from src/ui_framework/doc_site/src/views/button/button_basic.html rename to ui_framework/doc_site/src/views/button/button_basic.html diff --git a/src/ui_framework/doc_site/src/views/button/button_danger.html b/ui_framework/doc_site/src/views/button/button_danger.html similarity index 100% rename from src/ui_framework/doc_site/src/views/button/button_danger.html rename to ui_framework/doc_site/src/views/button/button_danger.html diff --git a/src/ui_framework/doc_site/src/views/button/button_elements.html b/ui_framework/doc_site/src/views/button/button_elements.html similarity index 100% rename from src/ui_framework/doc_site/src/views/button/button_elements.html rename to ui_framework/doc_site/src/views/button/button_elements.html diff --git a/src/ui_framework/doc_site/src/views/button/button_example.jsx b/ui_framework/doc_site/src/views/button/button_example.jsx similarity index 90% rename from src/ui_framework/doc_site/src/views/button/button_example.jsx rename to ui_framework/doc_site/src/views/button/button_example.jsx index 8f0b186b9575b0..7b32b9819bbe4f 100644 --- a/src/ui_framework/doc_site/src/views/button/button_example.jsx +++ b/ui_framework/doc_site/src/views/button/button_example.jsx @@ -11,6 +11,13 @@ export default createExample([{ ), html: require('./button_basic.html'), hasDarkTheme: false, +}, { + title: 'Hollow Button', + description: ( +

    Use the hollow Button when presenting a neutral action, e.g. a "Cancel" button.

    + ), + html: require('./button_hollow.html'), + hasDarkTheme: false, }, { title: 'Primary Button', description: ( diff --git a/src/ui_framework/doc_site/src/views/button/button_group.html b/ui_framework/doc_site/src/views/button/button_group.html similarity index 100% rename from src/ui_framework/doc_site/src/views/button/button_group.html rename to ui_framework/doc_site/src/views/button/button_group.html diff --git a/src/ui_framework/doc_site/src/views/button/button_group_united.html b/ui_framework/doc_site/src/views/button/button_group_united.html similarity index 100% rename from src/ui_framework/doc_site/src/views/button/button_group_united.html rename to ui_framework/doc_site/src/views/button/button_group_united.html diff --git a/ui_framework/doc_site/src/views/button/button_hollow.html b/ui_framework/doc_site/src/views/button/button_hollow.html new file mode 100644 index 00000000000000..0657a04f0aa4af --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_hollow.html @@ -0,0 +1,9 @@ + + +
    + + diff --git a/src/ui_framework/doc_site/src/views/button/button_primary.html b/ui_framework/doc_site/src/views/button/button_primary.html similarity index 100% rename from src/ui_framework/doc_site/src/views/button/button_primary.html rename to ui_framework/doc_site/src/views/button/button_primary.html diff --git a/src/ui_framework/doc_site/src/views/button/button_with_icon.html b/ui_framework/doc_site/src/views/button/button_with_icon.html similarity index 100% rename from src/ui_framework/doc_site/src/views/button/button_with_icon.html rename to ui_framework/doc_site/src/views/button/button_with_icon.html diff --git a/src/ui_framework/doc_site/src/views/button/buttons_in_tool_bar.html b/ui_framework/doc_site/src/views/button/buttons_in_tool_bar.html similarity index 100% rename from src/ui_framework/doc_site/src/views/button/buttons_in_tool_bar.html rename to ui_framework/doc_site/src/views/button/buttons_in_tool_bar.html diff --git a/src/ui_framework/doc_site/src/views/form/check_box.html b/ui_framework/doc_site/src/views/form/check_box.html similarity index 100% rename from src/ui_framework/doc_site/src/views/form/check_box.html rename to ui_framework/doc_site/src/views/form/check_box.html diff --git a/src/ui_framework/doc_site/src/views/form/form_example.jsx b/ui_framework/doc_site/src/views/form/form_example.jsx similarity index 82% rename from src/ui_framework/doc_site/src/views/form/form_example.jsx rename to ui_framework/doc_site/src/views/form/form_example.jsx index d058524ba8eab9..40cec4eec27a68 100644 --- a/src/ui_framework/doc_site/src/views/form/form_example.jsx +++ b/ui_framework/doc_site/src/views/form/form_example.jsx @@ -16,4 +16,8 @@ export default createExample([{ title: 'CheckBox', html: require('./check_box.html'), hasDarkTheme: false, +}, { + title: 'Select', + html: require('./select.html'), + hasDarkTheme: false, }]); diff --git a/ui_framework/doc_site/src/views/form/select.html b/ui_framework/doc_site/src/views/form/select.html new file mode 100644 index 00000000000000..e66327a785def4 --- /dev/null +++ b/ui_framework/doc_site/src/views/form/select.html @@ -0,0 +1,5 @@ + diff --git a/src/ui_framework/doc_site/src/views/form/text_area.html b/ui_framework/doc_site/src/views/form/text_area.html similarity index 100% rename from src/ui_framework/doc_site/src/views/form/text_area.html rename to ui_framework/doc_site/src/views/form/text_area.html diff --git a/src/ui_framework/doc_site/src/views/form/text_input.html b/ui_framework/doc_site/src/views/form/text_input.html similarity index 100% rename from src/ui_framework/doc_site/src/views/form/text_input.html rename to ui_framework/doc_site/src/views/form/text_input.html diff --git a/src/ui_framework/doc_site/src/views/home/_home_view.scss b/ui_framework/doc_site/src/views/home/_home_view.scss similarity index 100% rename from src/ui_framework/doc_site/src/views/home/_home_view.scss rename to ui_framework/doc_site/src/views/home/_home_view.scss diff --git a/src/ui_framework/doc_site/src/views/home/home_view.jsx b/ui_framework/doc_site/src/views/home/home_view.jsx similarity index 100% rename from src/ui_framework/doc_site/src/views/home/home_view.jsx rename to ui_framework/doc_site/src/views/home/home_view.jsx diff --git a/src/ui_framework/doc_site/src/views/icon/icon.html b/ui_framework/doc_site/src/views/icon/icon.html similarity index 100% rename from src/ui_framework/doc_site/src/views/icon/icon.html rename to ui_framework/doc_site/src/views/icon/icon.html diff --git a/src/ui_framework/doc_site/src/views/icon/icon_error.html b/ui_framework/doc_site/src/views/icon/icon_error.html similarity index 100% rename from src/ui_framework/doc_site/src/views/icon/icon_error.html rename to ui_framework/doc_site/src/views/icon/icon_error.html diff --git a/src/ui_framework/doc_site/src/views/icon/icon_example.jsx b/ui_framework/doc_site/src/views/icon/icon_example.jsx similarity index 100% rename from src/ui_framework/doc_site/src/views/icon/icon_example.jsx rename to ui_framework/doc_site/src/views/icon/icon_example.jsx diff --git a/src/ui_framework/doc_site/src/views/icon/icon_success.html b/ui_framework/doc_site/src/views/icon/icon_success.html similarity index 100% rename from src/ui_framework/doc_site/src/views/icon/icon_success.html rename to ui_framework/doc_site/src/views/icon/icon_success.html diff --git a/src/ui_framework/doc_site/src/views/icon/icon_warning.html b/ui_framework/doc_site/src/views/icon/icon_warning.html similarity index 100% rename from src/ui_framework/doc_site/src/views/icon/icon_warning.html rename to ui_framework/doc_site/src/views/icon/icon_warning.html diff --git a/src/ui_framework/doc_site/src/views/info_panel/info_panel.html b/ui_framework/doc_site/src/views/info_panel/info_panel.html similarity index 100% rename from src/ui_framework/doc_site/src/views/info_panel/info_panel.html rename to ui_framework/doc_site/src/views/info_panel/info_panel.html diff --git a/src/ui_framework/doc_site/src/views/info_panel/info_panel_example.jsx b/ui_framework/doc_site/src/views/info_panel/info_panel_example.jsx similarity index 100% rename from src/ui_framework/doc_site/src/views/info_panel/info_panel_example.jsx rename to ui_framework/doc_site/src/views/info_panel/info_panel_example.jsx diff --git a/src/ui_framework/doc_site/src/views/link/link.html b/ui_framework/doc_site/src/views/link/link.html similarity index 100% rename from src/ui_framework/doc_site/src/views/link/link.html rename to ui_framework/doc_site/src/views/link/link.html diff --git a/src/ui_framework/doc_site/src/views/link/link_example.jsx b/ui_framework/doc_site/src/views/link/link_example.jsx similarity index 100% rename from src/ui_framework/doc_site/src/views/link/link_example.jsx rename to ui_framework/doc_site/src/views/link/link_example.jsx diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_breadcrumbs.html b/ui_framework/doc_site/src/views/local_nav/local_nav_breadcrumbs.html similarity index 100% rename from src/ui_framework/doc_site/src/views/local_nav/local_nav_breadcrumbs.html rename to ui_framework/doc_site/src/views/local_nav/local_nav_breadcrumbs.html diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown.html b/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown.html similarity index 100% rename from src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown.html rename to ui_framework/doc_site/src/views/local_nav/local_nav_dropdown.html diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown_panels.html b/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown_panels.html similarity index 100% rename from src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown_panels.html rename to ui_framework/doc_site/src/views/local_nav/local_nav_dropdown_panels.html diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_example.jsx b/ui_framework/doc_site/src/views/local_nav/local_nav_example.jsx similarity index 100% rename from src/ui_framework/doc_site/src/views/local_nav/local_nav_example.jsx rename to ui_framework/doc_site/src/views/local_nav/local_nav_example.jsx diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_menu_item_states.html b/ui_framework/doc_site/src/views/local_nav/local_nav_menu_item_states.html similarity index 100% rename from src/ui_framework/doc_site/src/views/local_nav/local_nav_menu_item_states.html rename to ui_framework/doc_site/src/views/local_nav/local_nav_menu_item_states.html diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_search.html b/ui_framework/doc_site/src/views/local_nav/local_nav_search.html similarity index 100% rename from src/ui_framework/doc_site/src/views/local_nav/local_nav_search.html rename to ui_framework/doc_site/src/views/local_nav/local_nav_search.html diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_search_error.html b/ui_framework/doc_site/src/views/local_nav/local_nav_search_error.html similarity index 100% rename from src/ui_framework/doc_site/src/views/local_nav/local_nav_search_error.html rename to ui_framework/doc_site/src/views/local_nav/local_nav_search_error.html diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_simple.html b/ui_framework/doc_site/src/views/local_nav/local_nav_simple.html similarity index 100% rename from src/ui_framework/doc_site/src/views/local_nav/local_nav_simple.html rename to ui_framework/doc_site/src/views/local_nav/local_nav_simple.html diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_tabs.html b/ui_framework/doc_site/src/views/local_nav/local_nav_tabs.html similarity index 100% rename from src/ui_framework/doc_site/src/views/local_nav/local_nav_tabs.html rename to ui_framework/doc_site/src/views/local_nav/local_nav_tabs.html diff --git a/src/ui_framework/doc_site/src/views/micro_button/micro_button.html b/ui_framework/doc_site/src/views/micro_button/micro_button.html similarity index 100% rename from src/ui_framework/doc_site/src/views/micro_button/micro_button.html rename to ui_framework/doc_site/src/views/micro_button/micro_button.html diff --git a/src/ui_framework/doc_site/src/views/micro_button/micro_button_elements.html b/ui_framework/doc_site/src/views/micro_button/micro_button_elements.html similarity index 100% rename from src/ui_framework/doc_site/src/views/micro_button/micro_button_elements.html rename to ui_framework/doc_site/src/views/micro_button/micro_button_elements.html diff --git a/src/ui_framework/doc_site/src/views/micro_button/micro_button_example.jsx b/ui_framework/doc_site/src/views/micro_button/micro_button_example.jsx similarity index 100% rename from src/ui_framework/doc_site/src/views/micro_button/micro_button_example.jsx rename to ui_framework/doc_site/src/views/micro_button/micro_button_example.jsx diff --git a/src/ui_framework/doc_site/src/views/micro_button/micro_button_group.html b/ui_framework/doc_site/src/views/micro_button/micro_button_group.html similarity index 100% rename from src/ui_framework/doc_site/src/views/micro_button/micro_button_group.html rename to ui_framework/doc_site/src/views/micro_button/micro_button_group.html diff --git a/ui_framework/doc_site/src/views/modal/modal.html b/ui_framework/doc_site/src/views/modal/modal.html new file mode 100644 index 00000000000000..1f0c5be708d2be --- /dev/null +++ b/ui_framework/doc_site/src/views/modal/modal.html @@ -0,0 +1,25 @@ +
    +
    +
    + Delete object +
    + +
    +
    + +
    +
    + Are you sure you want to delete this object? You can’t undo this. +
    +
    + +
    + + + +
    +
    diff --git a/ui_framework/doc_site/src/views/modal/modal_example.jsx b/ui_framework/doc_site/src/views/modal/modal_example.jsx new file mode 100644 index 00000000000000..7df1dc0a094039 --- /dev/null +++ b/ui_framework/doc_site/src/views/modal/modal_example.jsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { + createExample, +} from '../../services'; + +export default createExample([{ + title: 'Modal', + html: require('./modal.html'), + hasDarkTheme: false, +}, { + title: 'ModalOverlay', + html: require('./modal_overlay.html'), + js: require('raw!./modal_overlay.js'), + hasDarkTheme: false, +}]); diff --git a/ui_framework/doc_site/src/views/modal/modal_overlay.html b/ui_framework/doc_site/src/views/modal/modal_overlay.html new file mode 100644 index 00000000000000..a9d62e6665387c --- /dev/null +++ b/ui_framework/doc_site/src/views/modal/modal_overlay.html @@ -0,0 +1,29 @@ + + +
    +
    +
    +
    + Delete object +
    + +
    +
    + +
    +
    + Are you sure you want to delete this object? You can’t undo this. +
    +
    + +
    + + + +
    +
    +
    diff --git a/ui_framework/doc_site/src/views/modal/modal_overlay.js b/ui_framework/doc_site/src/views/modal/modal_overlay.js new file mode 100644 index 00000000000000..1f9fb98d9073b1 --- /dev/null +++ b/ui_framework/doc_site/src/views/modal/modal_overlay.js @@ -0,0 +1,45 @@ +/* eslint-disable */ + +const $showModalOverlayButton = $('[data-id="showModalOverlay"]'); +const $modalOverlay = $('.kuiModalOverlay'); +const $modalOverlayCloseButton = $('.kuiModalOverlay .kuiModalHeaderCloseButton'); +const $modalOverlayCancelButton = $('.kuiModalOverlay .kuiButton--hollow'); +const $modalOverlayConfirmButton = $('.kuiModalOverlay .kuiButton--primary'); + +if (!$showModalOverlayButton.length) { + throw new Error('$showModalOverlayButton missing'); +} + +if (!$modalOverlay.length) { + throw new Error('$modalOverlay missing'); +} + +if (!$modalOverlayCloseButton.length) { + throw new Error('$modalOverlayCloseButton missing'); +} + +if (!$modalOverlayCancelButton.length) { + throw new Error('$modalOverlayCancelButton missing'); +} + +if (!$modalOverlayConfirmButton.length) { + throw new Error('$modalOverlayConfirmButton missing'); +} + +$modalOverlay.hide(); + +$showModalOverlayButton.on('click', () => { + $modalOverlay.show(); +}); + +$modalOverlayCloseButton.on('click', () => { + $modalOverlay.hide(); +}); + +$modalOverlayCancelButton.on('click', () => { + $modalOverlay.hide(); +}); + +$modalOverlayConfirmButton.on('click', () => { + $modalOverlay.hide(); +}); diff --git a/src/ui_framework/doc_site/src/views/not_found/not_found_view.jsx b/ui_framework/doc_site/src/views/not_found/not_found_view.jsx similarity index 100% rename from src/ui_framework/doc_site/src/views/not_found/not_found_view.jsx rename to ui_framework/doc_site/src/views/not_found/not_found_view.jsx diff --git a/src/ui_framework/doc_site/src/views/table/controlled_table.html b/ui_framework/doc_site/src/views/table/controlled_table.html similarity index 100% rename from src/ui_framework/doc_site/src/views/table/controlled_table.html rename to ui_framework/doc_site/src/views/table/controlled_table.html diff --git a/src/ui_framework/doc_site/src/views/table/controlled_table_loading_items.html b/ui_framework/doc_site/src/views/table/controlled_table_loading_items.html similarity index 100% rename from src/ui_framework/doc_site/src/views/table/controlled_table_loading_items.html rename to ui_framework/doc_site/src/views/table/controlled_table_loading_items.html diff --git a/src/ui_framework/doc_site/src/views/table/controlled_table_no_items.html b/ui_framework/doc_site/src/views/table/controlled_table_no_items.html similarity index 100% rename from src/ui_framework/doc_site/src/views/table/controlled_table_no_items.html rename to ui_framework/doc_site/src/views/table/controlled_table_no_items.html diff --git a/src/ui_framework/doc_site/src/views/table/table.html b/ui_framework/doc_site/src/views/table/table.html similarity index 100% rename from src/ui_framework/doc_site/src/views/table/table.html rename to ui_framework/doc_site/src/views/table/table.html diff --git a/src/ui_framework/doc_site/src/views/table/table.js b/ui_framework/doc_site/src/views/table/table.js similarity index 82% rename from src/ui_framework/doc_site/src/views/table/table.js rename to ui_framework/doc_site/src/views/table/table.js index 8395cd6a4b6e4a..d1d25a89c64f9c 100644 --- a/src/ui_framework/doc_site/src/views/table/table.js +++ b/ui_framework/doc_site/src/views/table/table.js @@ -6,6 +6,10 @@ $('[data-sort-icon-descending]').hide(); const demoSortableColumns = $('[data-demo-sortable-column]'); +if (!demoSortableColumns.length) { + throw new Error('demoSortableColumns missing'); +} + let sortedColumn; let isSortAscending = true; @@ -20,6 +24,15 @@ function sortColumn(column) { $sortedColumn.removeClass('tableHeaderCell-isSorted'); const ascendingIcon = $sortedColumn.find('[data-sort-icon-ascending]'); const descendingIcon = $sortedColumn.find('[data-sort-icon-descending]'); + + if (!ascendingIcon.length) { + throw new Error('ascendingIcon missing'); + } + + if (!descendingIcon.length) { + throw new Error('descendingIcon missing'); + } + ascendingIcon.hide(); descendingIcon.hide(); } @@ -29,8 +42,18 @@ function sortColumn(column) { sortedColumn = column; const $sortedColumn = $(sortedColumn); $sortedColumn.addClass('tableHeaderCell-isSorted'); + const ascendingIcon = $(sortedColumn).find('[data-sort-icon-ascending]'); const descendingIcon = $(sortedColumn).find('[data-sort-icon-descending]'); + + if (!ascendingIcon.length) { + throw new Error('ascendingIcon missing'); + } + + if (!descendingIcon.length) { + throw new Error('descendingIcon missing'); + } + if (isSortAscending) { ascendingIcon.show(); descendingIcon.hide(); diff --git a/src/ui_framework/doc_site/src/views/table/table_example.jsx b/ui_framework/doc_site/src/views/table/table_example.jsx similarity index 100% rename from src/ui_framework/doc_site/src/views/table/table_example.jsx rename to ui_framework/doc_site/src/views/table/table_example.jsx diff --git a/src/ui_framework/doc_site/src/views/tabs/tabs.html b/ui_framework/doc_site/src/views/tabs/tabs.html similarity index 100% rename from src/ui_framework/doc_site/src/views/tabs/tabs.html rename to ui_framework/doc_site/src/views/tabs/tabs.html diff --git a/src/ui_framework/doc_site/src/views/tabs/tabs.js b/ui_framework/doc_site/src/views/tabs/tabs.js similarity index 85% rename from src/ui_framework/doc_site/src/views/tabs/tabs.js rename to ui_framework/doc_site/src/views/tabs/tabs.js index a435dd6bf0e261..c3e6919f01779e 100644 --- a/src/ui_framework/doc_site/src/views/tabs/tabs.js +++ b/ui_framework/doc_site/src/views/tabs/tabs.js @@ -3,6 +3,10 @@ const $tabs = $('.kuiTab'); let $selectedTab = undefined; +if (!$tabs.length) { + throw new Error('$tabs missing'); +} + function selectTab(tab) { if ($selectedTab) { $selectedTab.removeClass('kuiTab-isSelected'); diff --git a/src/ui_framework/doc_site/src/views/tabs/tabs_example.jsx b/ui_framework/doc_site/src/views/tabs/tabs_example.jsx similarity index 100% rename from src/ui_framework/doc_site/src/views/tabs/tabs_example.jsx rename to ui_framework/doc_site/src/views/tabs/tabs_example.jsx diff --git a/src/ui_framework/doc_site/src/views/tool_bar/tool_bar.html b/ui_framework/doc_site/src/views/tool_bar/tool_bar.html similarity index 89% rename from src/ui_framework/doc_site/src/views/tool_bar/tool_bar.html rename to ui_framework/doc_site/src/views/tool_bar/tool_bar.html index f0cf58de308884..7cd8c1b7518714 100644 --- a/src/ui_framework/doc_site/src/views/tool_bar/tool_bar.html +++ b/ui_framework/doc_site/src/views/tool_bar/tool_bar.html @@ -8,6 +8,12 @@ placeholder="Search..." > + +
    @@ -25,6 +31,7 @@
    +
    1 – 20 of 33
    diff --git a/src/ui_framework/doc_site/src/views/tool_bar/tool_bar_example.jsx b/ui_framework/doc_site/src/views/tool_bar/tool_bar_example.jsx similarity index 83% rename from src/ui_framework/doc_site/src/views/tool_bar/tool_bar_example.jsx rename to ui_framework/doc_site/src/views/tool_bar/tool_bar_example.jsx index 036e5f97678d19..b2a3e3bd95feb4 100644 --- a/src/ui_framework/doc_site/src/views/tool_bar/tool_bar_example.jsx +++ b/ui_framework/doc_site/src/views/tool_bar/tool_bar_example.jsx @@ -11,6 +11,10 @@ export default createExample([{ ), html: require('./tool_bar.html'), hasDarkTheme: false, +}, { + title: 'ToolBar with Search only', + html: require('./tool_bar_search_only.html'), + hasDarkTheme: false, }, { title: 'ToolBarFooter', description: ( diff --git a/src/ui_framework/doc_site/src/views/tool_bar/tool_bar_footer.html b/ui_framework/doc_site/src/views/tool_bar/tool_bar_footer.html similarity index 100% rename from src/ui_framework/doc_site/src/views/tool_bar/tool_bar_footer.html rename to ui_framework/doc_site/src/views/tool_bar/tool_bar_footer.html diff --git a/ui_framework/doc_site/src/views/tool_bar/tool_bar_search_only.html b/ui_framework/doc_site/src/views/tool_bar/tool_bar_search_only.html new file mode 100644 index 00000000000000..ce8a04e9d635db --- /dev/null +++ b/ui_framework/doc_site/src/views/tool_bar/tool_bar_search_only.html @@ -0,0 +1,12 @@ +
    +
    +
    + + +
    +
    +
    diff --git a/src/ui_framework/doc_site/webpack.config.js b/ui_framework/doc_site/webpack.config.js similarity index 83% rename from src/ui_framework/doc_site/webpack.config.js rename to ui_framework/doc_site/webpack.config.js index 172ef1db2626c0..05ab102f210586 100644 --- a/src/ui_framework/doc_site/webpack.config.js +++ b/ui_framework/doc_site/webpack.config.js @@ -4,11 +4,11 @@ module.exports = { devtool: 'source-map', entry: { - guide: './src/ui_framework/doc_site/src/index.js' + guide: './ui_framework/doc_site/src/index.js' }, output: { - path: path.resolve(__dirname, 'src/ui_framework/doc_site/build'), + path: path.resolve(__dirname, 'ui_framework/doc_site/build'), filename: 'bundle.js' },
    .{{function.name}}() {{function.help}}
    - - - - - - - - - - - +
    Argument NameAccepted TypesInformation
    {{arg.name}}{{arg.types.join(', ')}}{{arg.help}}
    + + + + + + + + + +
    Argument NameAccepted TypesInformation
    {{arg.name}}{{arg.types.join(', ')}}{{arg.help}}
    - This function does not accept any arguments. Well that's simple, isn't it? + + This function does not accept any arguments. + Well that's simple, isn't it? +
    {{col.total | number}}