Skip to content

Commit

Permalink
Added saving and loading of visualizations from within the visualizat…
Browse files Browse the repository at this point in the history
…ion editor. #33
  • Loading branch information
Spencer Alger committed Apr 15, 2014
1 parent f804a45 commit 73874a4
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 115 deletions.
2 changes: 1 addition & 1 deletion src/kibana/apps/discover/saved_searches/_saved_search.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ define(function (require) {
mapping: {
title: 'string',
description: 'string',
hits: 'number',
hits: 'number'
},

defaults: {
Expand Down
1 change: 1 addition & 0 deletions src/kibana/apps/discover/saved_searches/saved_searches.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
125 changes: 85 additions & 40 deletions src/kibana/apps/visualize/controllers/editor.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
define(function (require) {
var _ = require('lodash');
var ConfigTemplate = require('utils/config_template');

require('../saved_visualizations/saved_visualizations');
require('notify/notify');
Expand All @@ -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':
Expand All @@ -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) {
Expand All @@ -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');
});
});
Binary file added src/kibana/apps/visualize/imgs/histogram.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 23 additions & 5 deletions src/kibana/apps/visualize/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
<div class="container-fluid">
<div ng-controller="VisualizeEditor">
<div class="row">
<div ng-controller="VisualizeEditor">
<nav class="navbar navbar-default navbar-static-top">
<div class="container-fluid">
<div class="navbar-header" ng-if="vis.title">
<span class="navbar-brand pull-left">
{{vis.title}}
</span>
</div>

<ul class="nav navbar-nav pull-left">
<li><a ng-click="startOver()" class="navbar-link"><i class="fa fa-file-o"></i></a></li>
<li><a ng-click="toggleSave()" class="navbar-link"><i class="fa fa-save"></i></a></li>
<li><a ng-click="toggleLoad()" class="navbar-link"><i class="fa fa-folder-open"></i></a></li>
<li><a class="navbar-link" ng-click="doVisualize()"><i class="fa fa-refresh"></i></a></li>
</ul>
</div>
</nav>
<config
config-template="configTemplate"
config-object="conf">
</config>
<div class="container-fluid">
<div class="row vis-content">
<div class="col-md-3">
<form>
<ul class="vis-config-panel">
Expand All @@ -12,8 +32,6 @@
</vis-config-category>
</li>
</ul>
<button type="button" ng-click="doVisualize()" class="btn btn-primary">Visualize</button>
<button type="button" ng-click="doSave()" class="btn btn-primary">Save</button>
</form>
</div>
<div class="col-md-9 vis-canvas">
Expand Down
20 changes: 20 additions & 0 deletions src/kibana/apps/visualize/partials/load.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<form ng-submit="false" role="form">
<div class="form-group">
<label for="loadVisFilter">Filter</label>
<input class="form-control" name="loadVisFilter" type="text" ng-model="conf.loadVis.filter">
</div>
</form>
<ng-switch on="conf.loadVis.list.length">
<ul ng-switch-default class="media-list vis-load-list">
<li ng-repeat="saved in conf.loadVis.list" class="media">
<a ng-href="#{{saved.url}}" class="pull-left">
<img ng-class="'media-object vis-preview-' + saved.typeName">
</a>
<div class="media-body">
<a ng-href="#{{saved.url}}"><h4 class="media-heading">{{saved.title}}</h4></a>
<p ng-if="saved.description">{{saved.description}}</p>
</div>
</li>
</ul>
<p ng-switch-when="0">No matching visualizations</p>
</ul>
12 changes: 12 additions & 0 deletions src/kibana/apps/visualize/partials/save.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<form role="form" ng-submit="conf.doSave()">
<div class="form-group">
<label for="visTitle">Title</label>
<input class="form-control" type="text" name="visTitle" ng-model="conf.vis.title" required>
</div>
<div class="form-group">
<label for="visDescription">Description</label>
<textarea class="form-control" name="visDescription" ng-model="conf.vis.description" placeholder=""></textarea>
</div>

<button type="submit" class="btn btn-primary">Save</button>
</form>
4 changes: 4 additions & 0 deletions src/kibana/apps/visualize/saved_visualizations/_saved_vis.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ define(function (require) {
var typeDefs = [
{
name: 'histogram',
previewUrl: '',
previewUrl: 'kibana/apps/visualize/imgs/histogram.jpg',
config: {
metric: {
label: 'Y-Axis',
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
});
});
};
});
});
Loading

0 comments on commit 73874a4

Please sign in to comment.