diff --git a/src/kibana/apps/discover/saved_searches/_saved_search.js b/src/kibana/apps/discover/saved_searches/_saved_search.js index ba2cd5d49cc3f6..613c4ba44e086f 100644 --- a/src/kibana/apps/discover/saved_searches/_saved_search.js +++ b/src/kibana/apps/discover/saved_searches/_saved_search.js @@ -19,7 +19,7 @@ define(function (require) { mapping: { title: 'string', description: 'string', - hits: 'number', + hits: 'number' }, defaults: { diff --git a/src/kibana/apps/discover/saved_searches/saved_searches.js b/src/kibana/apps/discover/saved_searches/saved_searches.js index 70d6860f9105d9..da4773201cd930 100644 --- a/src/kibana/apps/discover/saved_searches/saved_searches.js +++ b/src/kibana/apps/discover/saved_searches/saved_searches.js @@ -36,6 +36,7 @@ define(function (require) { .then(function (resp) { return resp.hits.hits.map(function (hit) { var source = hit._source; + source.id = hit._id; source.url = '/discover/' + hit._id; return source; }); diff --git a/src/kibana/apps/visualize/controllers/editor.js b/src/kibana/apps/visualize/controllers/editor.js index 5d1fdabd497185..c5f20761f2500f 100644 --- a/src/kibana/apps/visualize/controllers/editor.js +++ b/src/kibana/apps/visualize/controllers/editor.js @@ -1,5 +1,6 @@ define(function (require) { var _ = require('lodash'); + var ConfigTemplate = require('utils/config_template'); require('../saved_visualizations/saved_visualizations'); require('notify/notify'); @@ -12,54 +13,46 @@ define(function (require) { var aggs = require('../saved_visualizations/_aggs'); var visConfigCategories = require('../saved_visualizations/_config_categories'); - app.controller('VisualizeEditor', function ($route, $scope, courier, createNotifier, config, $location) { + app.controller('VisualizeEditor', function ($route, $scope, courier, createNotifier, config, $location, savedVisualizations) { var notify = createNotifier({ location: 'Visualization Editor' }); + // get the vis loaded in from the routes var vis = $route.current.locals.vis; - $scope.refreshFields = function () { - $scope.fields = null; - vis.searchSource.clearFieldCache().then(getFields, notify.error); - }; - - function getFields() { - return vis.searchSource.getFields() - .then(function (fieldsHash) { - // create a sorted list of the fields for display purposes - $scope.fields = _(fieldsHash) - .keys() - .sort() - .transform(function (fields, name) { - var field = fieldsHash[name]; - field.name = name; - fields.push(field); - }) - .value(); - - $scope.fields.byName = fieldsHash; - }); - } - - getFields(); + // get the current field list + vis.searchSource.getFields() + .then(function (fieldsHash) { + // create a sorted list of the fields for display purposes + $scope.fields = _(fieldsHash) + .keys() + .sort() + .transform(function (fields, name) { + var field = fieldsHash[name]; + field.name = name; + fields.push(field); + }) + .value(); + + $scope.fields.byName = fieldsHash; + }); $scope.vis = vis; $scope.aggs = aggs; $scope.visConfigCategories = visConfigCategories; - $scope.$on('change:config.defaultIndex', function () { - if (!vis.searchSource.get('index')) { - vis.searchSource.index(config.get('defaultIndex')); - getFields(); - } - }); - + /** + * (Re)set the aggs key on the vis.searchSource based on the + * current config + */ var updateDataSource = function () { notify.event('update data source'); + // get the config objects form the visualization var config = vis.getConfig(); + // group the segments by their categoryName, but merge the segments and groups config = _.groupBy(config, function (config) { switch (config.categoryName) { case 'group': @@ -70,17 +63,21 @@ define(function (require) { } }); + // use the global aggregation if we don't have any dimensions if (!config.dimension) { - // use the global aggregation if we don't have any dimensions config.dimension = [{ agg: 'global', aggParams: {} }]; } + // stores the config objects in queryDsl var dsl = {}; + // counter to ensure unique agg names var i = 0; + // continue to nest the aggs under each other + // writes to the dsl object var nest = (function () { var current = dsl; return function (config) { @@ -94,31 +91,79 @@ define(function (require) { }; }()); + // nest each config type in order config.split && config.split.forEach(nest); config.dimension && config.dimension.forEach(nest); config.metric && config.metric.forEach(nest); - notify.log('config', config); - notify.log('aggs', dsl.aggs); - + // set the dsl to the searchSource vis.searchSource.aggs(dsl.aggs); notify.event('update data source', true); }; - /********* - *** BUTTON EVENT HANDLERS - *********/ + /** + * Refresh Visualization + */ $scope.doVisualize = function () { updateDataSource(); vis.searchSource.fetch(); }; + + /** + * Restart on a new visualization + */ + $scope.startOver = function () { + $location.url('/visualize'); + }; + + /** + * Save the current vis state + */ $scope.doSave = function () { updateDataSource(); vis.save() .then(function () { - $location.url('/visualize/' + vis.typeName + '/' + vis.get('id')); + $location.url('/visualize/' + vis.typeName + '/' + vis.id); }, notify.fatal); }; + // templates that can be used in the config panel + var configTemplate = $scope.configTemplate = new ConfigTemplate({ + save: require('text!../partials/save.html'), + load: require('text!../partials/load.html') + }); + + /** + * Toggle the save config panel + */ + $scope.toggleSave = function () { + configTemplate.toggle('save'); + }; + + // stash for vars related to loading a vis + var loadVis = $scope.loadVis = { + filter: '', + list: [], + setList: function (hits) { + $scope.loadVis.list = hits.filter(function (hit) { + return hit.id !== $scope.vis.id; + }); + } + }; + + /** + * Toggle the load config panel + */ + $scope.toggleLoad = function () { + configTemplate.toggle('load') && savedVisualizations.find().then(loadVis.setList); + }; + + // when the filter changes, load in the new vis objects + $scope.$watch('loadVis.filter', function () { + savedVisualizations.find(loadVis.filter).then(loadVis.setList); + }); + + // objects to make available within the config panel's scope + $scope.conf = _.pick($scope, 'doSave', 'doLoad', 'loadVis', 'vis'); }); }); \ No newline at end of file diff --git a/src/kibana/apps/visualize/imgs/histogram.jpg b/src/kibana/apps/visualize/imgs/histogram.jpg new file mode 100644 index 00000000000000..219be5f19bda5a Binary files /dev/null and b/src/kibana/apps/visualize/imgs/histogram.jpg differ diff --git a/src/kibana/apps/visualize/index.html b/src/kibana/apps/visualize/index.html index 1a69ee451e76d6..de896f3590c94b 100644 --- a/src/kibana/apps/visualize/index.html +++ b/src/kibana/apps/visualize/index.html @@ -1,6 +1,26 @@ -
-
-
+
+ + + +
+
    @@ -12,8 +32,6 @@
- -
diff --git a/src/kibana/apps/visualize/partials/load.html b/src/kibana/apps/visualize/partials/load.html new file mode 100644 index 00000000000000..30819a2917ff5e --- /dev/null +++ b/src/kibana/apps/visualize/partials/load.html @@ -0,0 +1,20 @@ +
+
+ + +
+
+ + +

No matching visualizations

+ \ No newline at end of file diff --git a/src/kibana/apps/visualize/partials/save.html b/src/kibana/apps/visualize/partials/save.html new file mode 100644 index 00000000000000..d0dfc43b405490 --- /dev/null +++ b/src/kibana/apps/visualize/partials/save.html @@ -0,0 +1,12 @@ +
+
+ + +
+
+ + +
+ + +
\ No newline at end of file diff --git a/src/kibana/apps/visualize/saved_visualizations/_saved_vis.js b/src/kibana/apps/visualize/saved_visualizations/_saved_vis.js index 2289802378148d..0d913967db04fd 100644 --- a/src/kibana/apps/visualize/saved_visualizations/_saved_vis.js +++ b/src/kibana/apps/visualize/saved_visualizations/_saved_vis.js @@ -21,12 +21,16 @@ define(function (require) { id: id, mapping: { + title: 'string', + description: 'string', stateJSON: 'string', savedSearchId: 'string', typeName: 'string', }, defaults: { + title: '', + description: '', stateJSON: '{}', typeName: type, }, diff --git a/src/kibana/apps/visualize/saved_visualizations/_type_defs.js b/src/kibana/apps/visualize/saved_visualizations/_type_defs.js index 842a5b54fa7434..5809bc7b1ddc9f 100644 --- a/src/kibana/apps/visualize/saved_visualizations/_type_defs.js +++ b/src/kibana/apps/visualize/saved_visualizations/_type_defs.js @@ -5,7 +5,7 @@ define(function (require) { var typeDefs = [ { name: 'histogram', - previewUrl: '', + previewUrl: 'kibana/apps/visualize/imgs/histogram.jpg', config: { metric: { label: 'Y-Axis', diff --git a/src/kibana/apps/visualize/saved_visualizations/saved_visualizations.js b/src/kibana/apps/visualize/saved_visualizations/saved_visualizations.js index 93fe7d079507a0..c425ad4588a30e 100644 --- a/src/kibana/apps/visualize/saved_visualizations/saved_visualizations.js +++ b/src/kibana/apps/visualize/saved_visualizations/saved_visualizations.js @@ -1,11 +1,38 @@ define(function (require) { var app = require('modules').get('app/visualize'); + var typeDefs = require('./_type_defs'); require('./_saved_vis'); - app.service('savedVisualizations', function (es, courier, $q, $timeout, SavedVis) { + app.service('savedVisualizations', function (es, config, courier, $q, $timeout, SavedVis) { this.get = function (type, id) { return (new SavedVis(type, id)).init(); }; + + this.find = function (searchString) { + return es.search({ + index: config.file.kibanaIndex, + type: 'visualization', + body: { + query: { + multi_match: { + query: searchString || '', + type: 'phrase_prefix', + fields: ['title^3', 'description'], + zero_terms_query: 'all' + } + } + } + }) + .then(function (resp) { + return resp.hits.hits.map(function (hit) { + var source = hit._source; + source.id = hit._id; + source.url = '/visualize/' + source.typeName + '/' + hit._id; + source.typeDef = typeDefs.byName[source.typeName]; + return source; + }); + }); + }; }); }); \ No newline at end of file diff --git a/src/kibana/apps/visualize/styles/main.css b/src/kibana/apps/visualize/styles/main.css index 560d5bc8d914e6..90c7659ffdacf5 100644 --- a/src/kibana/apps/visualize/styles/main.css +++ b/src/kibana/apps/visualize/styles/main.css @@ -68,51 +68,51 @@ .modal-footer:after { clear: both; } -.vis-config-panel { +.vis-content { + padding: 15px 0; +} +.vis-content .vis-config-panel { padding: 0; } -.vis-config-panel .panel-heading { +.vis-content .vis-config-panel .panel-heading { position: relative; } -.vis-config-panel .panel-heading button { +.vis-content .vis-config-panel .panel-heading button { position: absolute; top: 6px; right: 7px; padding: 3px 8px; } -.vis-config-panel .panel-body { +.vis-content .vis-config-panel .panel-body { padding: 10px 0; } -.vis-config-panel vis-config-editor { +.vis-content .vis-config-panel vis-config-editor { display: block; border-top: 1px dashed #dddddd; padding: 10px 10px 0; } -.vis-config-panel vis-config-editor:first-child { +.vis-content .vis-config-panel vis-config-editor:first-child { border-top: none; } -.vis-config-panel vis-config-editor .config-controls { +.vis-content .vis-config-panel vis-config-editor .config-controls { float: right; margin-top: -2px; } -.vis-config-panel vis-config-editor .config-controls button { +.vis-content .vis-config-panel vis-config-editor .config-controls button { font-size: 10px; padding: 2px 4px; font-weight: 100; } -.vis-config-panel > li { +.vis-content .vis-config-panel > li { list-style: none; } -.vis-config-panel .agg-config-interval td { +.vis-content .vis-config-panel .agg-config-interval td { padding-left: 10px; } -.vis-config-panel .agg-config-interval td:first-child { +.vis-content .vis-config-panel .agg-config-interval td:first-child { padding-left: 0px; } -.vis-canvas { - padding: 15px 0; -} -.vis-canvas visualization { +.vis-content .vis-canvas visualization { display: block; background: black; height: 800px; @@ -120,3 +120,11 @@ margin: 0 auto; overflow: auto; } +.vis-load-list .media-object { + width: 65px; + height: 65px; +} +.vis-preview-histogram { + background-image: url(../imgs/histogram.jpg); + background-size: contain; +} diff --git a/src/kibana/apps/visualize/styles/main.less b/src/kibana/apps/visualize/styles/main.less index 2baddf06007cab..1112b59340c484 100644 --- a/src/kibana/apps/visualize/styles/main.less +++ b/src/kibana/apps/visualize/styles/main.less @@ -1,69 +1,83 @@ @import (reference) "../../../styles/_bootstrap.less"; @import (reference) "../../../styles/theme/_theme.less"; -.vis-config-panel { - padding: 0; +.vis-content { + padding: 15px 0; + + .vis-config-panel { + padding: 0; - .panel-heading { - position: relative; + .panel-heading { + position: relative; - button { - position: absolute; - top: 6px; - right: 7px; - padding: 3px 8px; + button { + position: absolute; + top: 6px; + right: 7px; + padding: 3px 8px; + } } - } - .panel-body { - padding: 10px 0; - } + .panel-body { + padding: 10px 0; + } - vis-config-editor { - display: block; - border-top: 1px dashed @panel-inner-border; - padding: 10px 10px 0; + vis-config-editor { + display: block; + border-top: 1px dashed @panel-inner-border; + padding: 10px 10px 0; - &:first-child { - border-top: none; - } + &:first-child { + border-top: none; + } - .config-controls { - float: right; - // just a tiny boost up - margin-top: -2px; + .config-controls { + float: right; + // just a tiny boost up + margin-top: -2px; - button { - font-size: 10px; - padding: 2px 4px; - font-weight: 100; + button { + font-size: 10px; + padding: 2px 4px; + font-weight: 100; + } } } - } - > li { - list-style: none; - } + > li { + list-style: none; + } - .agg-config-interval { - td { - padding-left: 10px; - &:first-child { - padding-left: 0px; + .agg-config-interval { + td { + padding-left: 10px; + &:first-child { + padding-left: 0px; + } } } } -} -.vis-canvas { - padding: 15px 0; + .vis-canvas { + visualization { + display: block; + background: black; + height: 800px; + width: 95%; + margin: 0 auto; + overflow: auto; + } + } +} - visualization { - display: block; - background: black; - height: 800px; - width: 95%; - margin: 0 auto; - overflow: auto; +.vis-load-list { + .media-object { + width: 65px; + height: 65px; } +} + +.vis-preview-histogram { + background-image: url(../imgs/histogram.jpg); + background-size: contain; } \ No newline at end of file diff --git a/src/kibana/directives/config.js b/src/kibana/directives/config.js index 71ae54588b0251..69e733b24ae956 100644 --- a/src/kibana/directives/config.js +++ b/src/kibana/directives/config.js @@ -26,7 +26,15 @@ define(function (require) { link: function ($scope, element, attr) { $scope[attr.configObject] = $scope.configObject; - $scope.$watch('configTemplate.current || configTemplate', function (newTemplate, oldTemplate) { + var wrapTmpl = function (tmpl) { + if ($scope.configSubmit) { + return '
' + tmpl + '
'; + } else { + return '
' + tmpl + '
'; + } + }; + + var render = function (newTemplate, oldTemplate) { var tmpl = $scope.configTemplate; if (tmpl instanceof ConfigTemplate) { tmpl = tmpl.toString(); @@ -34,16 +42,17 @@ define(function (require) { element.html(!tmpl ? '' : $compile('' + '
' + - '
' + - tmpl + - '
' + + wrapTmpl(tmpl) + '
' + ' ' + '
' + '
' + '' )($scope)); - }); + }; + + $scope.$watch('configSubmit', render); + $scope.$watch('configTemplate.current || configTemplate', render); $scope.close = function () { if (_.isFunction($scope.configClose)) $scope.configClose();