Skip to content

Commit

Permalink
Add comments and inline docs for visualization saving and editing pro…
Browse files Browse the repository at this point in the history
…cess.

The goal is to clarify where URL state is coming from, when working with
visualizations. Some of the classes touched upon are: SavedVis,
PersistedState, AppState, and base classes.

Explored files:
- src/core_plugins/kibana/public/visualize/editor/editor.js
- src/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js
- src/ui/public/courier/data_source/_doc_send_to_es.js
- src/ui/public/courier/data_source/doc_source.js
- src/ui/public/courier/saved_object/saved_object.js
- src/ui/public/es.js
- src/ui/public/events.js
- src/ui/public/persisted_state/persisted_state.js
- src/ui/public/state_management/app_state.js
- src/ui/public/state_management/state.js
- src/ui/public/vis/agg_config.js
- src/ui/public/vis/agg_configs.js
- src/ui/public/vis/vis.js
  • Loading branch information
cjcenizal committed Oct 11, 2016
1 parent e78c829 commit 34e3f49
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 10 deletions.
38 changes: 33 additions & 5 deletions src/core_plugins/kibana/public/visualize/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import uiRoutes from 'ui/routes';
import uiModules from 'ui/modules';
import editorTemplate from 'plugins/kibana/visualize/editor/editor.html';


uiRoutes
.when('/visualize/create', {
template: editorTemplate,
Expand Down Expand Up @@ -77,16 +76,29 @@ function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $tim
let stateMonitor;
const $appStatus = this.appStatus = {};

// Retrieve the resolved SavedVis instance.
const savedVis = $route.current.locals.savedVis;

// Instance of src/ui/public/vis/vis.js.
const vis = savedVis.vis;

// Clone the _vis instance.
const editableVis = vis.createEditableVis();

// We intend to keep editableVis and vis in sync with one another, so calling `requesting` on
// vis should call it on both.
vis.requesting = function () {
const requesting = editableVis.requesting;
// Invoking requesting() calls onRequest on each agg's type param. When a vis is marked as being
// requested, the bounds of that vis are updated and new data is fetched using the new bounds.
requesting.call(vis);

// We need to keep editableVis in sync with vis.
requesting.call(editableVis);
};

// SearchSource is a promise-based stream of search results that can inherit from other search
// sources.
const searchSource = savedVis.searchSource;

$scope.topNavMenu = [{
Expand Down Expand Up @@ -115,6 +127,8 @@ function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $tim
docTitle.change(savedVis.title);
}

// Extract visualization state with filtered aggs. You can see these filtered aggs in the URL.
// Consists of things like aggs, params, listeners, title, type, etc.
const savedVisState = vis.getState();
const stateDefaults = {
uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {},
Expand All @@ -124,20 +138,25 @@ function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $tim
vis: savedVisState
};

// Instance of app_state.js.
let $state = $scope.$state = (function initState() {
$state = new AppState(stateDefaults);
// This is used to sync visualization state with the url when `appState.save()` is called.
const appState = new AppState(stateDefaults);

if (!angular.equals($state.vis, savedVisState)) {
// The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the
// defaults applied. If the url was from a previous session which included modifications to the
// appState then they won't be equal.
if (!angular.equals(appState.vis, savedVisState)) {
Promise.try(function () {
editableVis.setState($state.vis);
editableVis.setState(appState.vis);
vis.setState(editableVis.getEnabledState());
})
.catch(courier.redirectWhenMissing({
'index-pattern-field': '/visualize'
}));
}

return $state;
return appState;
}());

function init() {
Expand All @@ -148,10 +167,16 @@ function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $tim
$scope.indexPattern = vis.indexPattern;
$scope.editableVis = editableVis;
$scope.state = $state;

// Create a PersistedState instance.
$scope.uiState = $state.makeStateful('uiState');
$scope.appStatus = $appStatus;

// 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');

Expand Down Expand Up @@ -258,6 +283,9 @@ function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $tim
kbnUrl.change('/visualize', {});
};

/**
* Called when the user clicks "Save" button.
*/
$scope.doSave = function () {
savedVis.id = savedVis.title;
// vis.title was not bound and it's needed to reflect title into visState
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
/**
* @name SavedVis
*
* @extends SavedObject.
*
* NOTE: It's a type of SavedObject, but specific to visualizations.
*/

import _ from 'lodash';
import VisProvider from 'ui/vis';
import uiModules from 'ui/modules';

uiModules
.get('app/visualize')
.factory('SavedVis', function (config, $injector, courier, Promise, savedSearches, Private, Notifier) {
Expand Down
7 changes: 7 additions & 0 deletions src/ui/public/courier/data_source/_doc_send_to_es.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* @name _doc_send_to_es
*
* NOTE: Depends upon the es object to make ES requests, and also interacts
* with courier objects.
*/

import _ from 'lodash';

import errors from 'ui/errors';
Expand Down
10 changes: 10 additions & 0 deletions src/ui/public/courier/data_source/doc_source.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
/**
* @name DocSource
*
* NOTE: This class is tightly coupled with _doc_send_to_es. Its primary
* methods (`doUpdate`, `doIndex`, `doCreate`) are all proxies for methods
* exposed by _doc_send_to_es (`update`, `index`, `create`). These methods are
* called with DocSource as the context. When called, they depend on “private”
* DocSource methods within their execution.
*/

import _ from 'lodash';

import 'ui/es';
Expand Down
52 changes: 52 additions & 0 deletions src/ui/public/courier/data_source/search_source.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,55 @@
/**
* @name SearchSource
*
* @description A promise-based stream of search results that can inherit from other search sources.
*
* Because filters/queries in Kibana have different levels of persistence and come from different
* places, it is important to keep track of where filters come from for when they are saved back to
* the savedObject store in the Kibana index. To do this, we create trees of searchSource objects
* that can have associated query parameters (index, query, filter, etc) which can also inherit from
* other searchSource objects.
*
* At query time, all of the searchSource objects that have subscribers are "flattened", at which
* point the query params from the searchSource are collected while traversing up the inheritance
* chain. At each link in the chain a decision about how to merge the query params is made until a
* single set of query parameters is created for each active searchSource (a searchSource with
* subscribers).
*
* That set of query parameters is then sent to elasticsearch. This is how the filter hierarchy
* works in Kibana.
*
* Visualize, starting from a new search:
*
* - the `savedVis.searchSource` is set as the `appSearchSource`.
* - The `savedVis.searchSource` would normally inherit from the `appSearchSource`, but now it is
* upgraded to inherit from the `rootSearchSource`.
* - Any interaction with the visualization will still apply filters to the `appSearchSource`, so
* they will be stored directly on the `savedVis.searchSource`.
* - Any interaction with the time filter will be written to the `rootSearchSource`, so those
* filters will not be saved by the `savedVis`.
* - When the `savedVis` is saved to elasticsearch, it takes with it all the filters that are
* defined on it directly, but none of the ones that it inherits from other places.
*
* Visualize, starting from an existing search:
*
* - The `savedVis` loads the `savedSearch` on which it is built.
* - The `savedVis.searchSource` is set to inherit from the `saveSearch.searchSource` and set as
* the `appSearchSource`.
* - The `savedSearch.searchSource`, is set to inherit from the `rootSearchSource`.
* - Then the `savedVis` is written to elasticsearch it will be flattened and only include the
* filters created in the visualize application and will reconnect the filters from the
* `savedSearch` at runtime to prevent losing the relationship
*
* Dashboard search sources:
*
* - Each panel in a dashboard has a search source.
* - The `savedDashboard` also has a searchsource, and it is set as the `appSearchSource`.
* - Each panel's search source inherits from the `appSearchSource`, meaning that they inherit from
* the dashboard search source.
* - When a filter is added to the search box, or via a visualization, it is written to the
* `appSearchSource`.
*/

import _ from 'lodash';

import NormalizeSortRequestProvider from './_normalize_sort_request';
Expand Down
11 changes: 11 additions & 0 deletions src/ui/public/courier/saved_object/saved_object.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
/**
* @name SavedObject
*
* NOTE: SavedObject seems to track a reference to an object in ES,
* and surface methods for CRUD functionality (save and delete). This seems
* similar to how Backbone Models work.
*
* This class seems to interface with ES primarily through the es Angular
* service and a DocSource instance.
*/

import angular from 'angular';
import _ from 'lodash';

Expand Down
7 changes: 7 additions & 0 deletions src/ui/public/es.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* @name es
*
* @description This is the result of calling esFactory. esFactory is exposed by the
* elasticsearch.angular.js client.
*/

import 'elasticsearch-browser';
import _ from 'lodash';
import uiModules from 'ui/modules';
Expand Down
6 changes: 6 additions & 0 deletions src/ui/public/events.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* @name Events
*
* @extends SimpleEmitter
*/

import _ from 'lodash';
import Notifier from 'ui/notify/notifier';
import SimpleEmitter from 'ui/utils/simple_emitter';
Expand Down
8 changes: 7 additions & 1 deletion src/ui/public/persisted_state/persisted_state.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* @name PersistedState
*
* @extends Events
*/

import _ from 'lodash';
import toPath from 'lodash/internal/toPath';
import errors from 'ui/errors';
Expand Down Expand Up @@ -269,4 +275,4 @@ export default function (Private) {
};

return PersistedState;
};
};
21 changes: 20 additions & 1 deletion src/ui/public/state_management/app_state.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
/**
* @name AppState
*
* @extends State
*
* @description Inherits State, which inherits Events. This class seems to be
* concerned with mapping "props" to PersistedState instances, and surfacing the
* ability to destroy those mappings.
*/

import _ from 'lodash';
import modules from 'ui/modules';
import StateManagementStateProvider from 'ui/state_management/state';
Expand All @@ -12,7 +22,13 @@ function AppStateProvider(Private, $rootScope, $location) {

_.class(AppState).inherits(State);
function AppState(defaults) {
// Initialize persistedStates. This object maps "prop" names to
// PersistedState instances. These are used to make properties "stateful".
persistedStates = {};

// Initialize eventUnsubscribers. These will be called in `destroy`, to
// remove handlers for the 'change' and 'fetch_with_changes' events which
// are dispatched via the rootScope.
eventUnsubscribers = [];

AppState.Super.call(this, urlParam, defaults);
Expand All @@ -28,6 +44,9 @@ function AppStateProvider(Private, $rootScope, $location) {
_.callEach(eventUnsubscribers);
};

/**
* @returns PersistedState instance.
*/
AppState.prototype.makeStateful = function (prop) {
if (persistedStates[prop]) return persistedStates[prop];
let self = this;
Expand All @@ -38,8 +57,8 @@ function AppStateProvider(Private, $rootScope, $location) {
// update the app state when the stateful instance changes
let updateOnChange = function () {
let replaceState = false; // TODO: debouncing logic

self[prop] = persistedStates[prop].getChanges();
// Save state to the URL.
self.save(replaceState);
};
let handlerOnChange = (method) => persistedStates[prop][method]('change', updateOnChange);
Expand Down
8 changes: 8 additions & 0 deletions src/ui/public/state_management/state.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* @name State
*
* @extends Events
*
* @description Persists generic "state" to and reads it from the URL.
*/

import _ from 'lodash';
import angular from 'angular';
import rison from 'rison-node';
Expand Down
11 changes: 9 additions & 2 deletions src/ui/public/vis/agg_config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* @name AggConfig
*
* @description This class represents an aggregation, which is displayed in the left-hand nav of
* the Visualize app.
*/

import _ from 'lodash';
import RegistryFieldFormatsProvider from 'ui/registry/field_formats';
export default function AggConfigFactory(Private, fieldTypeFilter) {
Expand Down Expand Up @@ -177,7 +184,7 @@ export default function AggConfigFactory(Private, fieldTypeFilter) {

/**
* Hook into param onRequest handling, and tell the aggConfig that it
* is being sent to elasticsearc.
* is being sent to elasticsearch.
*
* @return {[type]} [description]
*/
Expand All @@ -189,7 +196,7 @@ export default function AggConfigFactory(Private, fieldTypeFilter) {
};

/**
* Convert this aggConfig to it's dsl syntax.
* Convert this aggConfig to its dsl syntax.
*
* Adds params and adhoc subaggs to a pojo, then returns it
*
Expand Down
9 changes: 9 additions & 0 deletions src/ui/public/vis/agg_configs.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
/**
* @name AggConfig
*
* @extends IndexedArray
*
* @description A "data structure"-like class with methods for indexing and
* accessing instances of AggConfig.
*/

import _ from 'lodash';
import IndexedArray from 'ui/indexed_array';
import VisAggConfigProvider from 'ui/vis/agg_config';
Expand Down
Loading

0 comments on commit 34e3f49

Please sign in to comment.