Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add comments and inline docs for visualization saving and editing process. #7968

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions src/core_plugins/kibana/public/visualize/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,27 @@ uiModules
location: 'Visualization Editor'
});

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

// Instance of vis.js.
const vis = savedVis.vis;

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

// NOTE: Why is this instance method being overwritten?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spalger Question

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The editableVis is intended to be a mirror of the vis object, and as such it needs to go through the same lifecycle methods that vis does (in this case #requesting()). I'm not certain what benefits from the requesting method being called on the editable vis though...

vis.requesting = function () {
const requesting = editableVis.requesting;
// Invoking requesting() calls onRequest on each agg's type param.
// NOTE: What is the structure of an agg_config, and the role of type
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spalger Question

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AggConfig objects represent the aggs defined in the left-hand nav of the visualize app. Some of those AggConfig objects need to know properties about the context of the app at the time that the visualization is requested, like the date histogram for instance which tracks the bounds of the time filter at the time of each request.

When a vis is marked as being requested, the bounds of that vis are updated and the corresponding data can be resolved based on new bounds.

// and type.params?
requesting.call(vis);
requesting.call(editableVis);
};

// Boolean by default, but then something else later
// NOTE: What is the role of searchSource? Why does it default to a boolean?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spalger Question

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is kind of a scattershot description, I'll probably come back and edit it to be more coherent later

SearchSources are essentially promise-based streams 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:

  1. the savedVis.searchSource is set as the appSearchSource(currently managed here)
  2. The savedVis.searchSource would normally inherit from the appSearchSource, but now it is upgraded to inherit from the rootSearchSource
  3. Any interaction with the visualization will still apply filters to the appSearchSource, so they will be stored directly on the savedVis.searchSource
  4. Any interaction with the time filter will be written to the rootSearchSource, so those filters will not be saved by the savedVis
  5. 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:

  1. The savedVis loads the savedSearch that it is built on top of
  2. The savedVis.searchSource is set to inherit from the saveSearch.searchSource and set as the appSearchSource
  3. the savedSearch.searchSource, is set to inherit from the rootSearchSource
  4. when 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:

  1. each panel in a dashboard has a search source
  2. the savedDashboard also has a searchsource, and it is set as the appSearchSource
  3. each panel's search source inherits from the appSearchSource, meaning that they inherit from the dashboard search source.
  4. when a filter is added to the search box, or via a visualization, it is written to the appSearchSource

const searchSource = savedVis.searchSource;

$scope.topNavMenu = [{
Expand Down Expand Up @@ -104,8 +115,12 @@ uiModules
docTitle.change(savedVis.title);
}

Copy link
Contributor Author

@cjcenizal cjcenizal Aug 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed like a mistake, so to clarify the role of this variable within the function I gave it a unique name and scoped it to the function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, wow, confused why eslint didn't complain about that.

// Instance of app_state.js.
let $state = $scope.$state = (function initState() {
// Extract visualization state with filtered aggs.
// Consists of: aggs, params, listeners, title, and type.
const savedVisState = vis.getState();

const stateDefaults = {
uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {},
linked: !!savedVis.savedSearchId,
Expand All @@ -114,19 +129,22 @@ uiModules
vis: savedVisState
};

$state = new AppState(stateDefaults);
// This is used to generate a 'uiState' PersistedState instance, and
// implictly add 'change' and 'fetch_with_changes' event handlers.
const appState = new AppState(stateDefaults);

if (!angular.equals($state.vis, savedVisState)) {
// NOTE: Why would appState.vis not equal the savedVisState?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spalger Question

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @@ -137,8 +155,14 @@ uiModules
$scope.indexPattern = vis.indexPattern;
$scope.editableVis = editableVis;
$scope.state = $state;

// Create a PersistedState instance.
$scope.uiState = $state.makeStateful('uiState');
// 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 @@ -239,6 +263,9 @@ uiModules
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,16 @@

/**
* @name SavedVis
*
* @extends SavedObject.
*
* @description 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
8 changes: 8 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,11 @@

/**
* @name _doc_send_to_es
*
* @description 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
11 changes: 11 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,14 @@

/**
* @name DocSource
*
* @description 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
12 changes: 12 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,15 @@

/**
* @name SavedObject
*
* @description 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
6 changes: 6 additions & 0 deletions src/ui/public/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import 'elasticsearch-browser';
import _ from 'lodash';
import uiModules from 'ui/modules';

/**
* @name es
* @description This is the result of calling esFactory. Where is esFactory
* defined?
*/

let es; // share the client amongst all apps
uiModules
.get('kibana', ['elasticsearch', 'kibana/config'])
Expand Down
7 changes: 7 additions & 0 deletions src/ui/public/events.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@

/**
* @name Events
*
* @extends SimpleEmitter
*/

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

/**
* @name PersistedState
*
* @extends Events
*/

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

return PersistedState;
};
};
22 changes: 21 additions & 1 deletion src/ui/public/state_management/app_state.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@

/**
* @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 +23,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 +45,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 +58,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
9 changes: 9 additions & 0 deletions src/ui/public/state_management/state.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@

/**
* @name State
*
* @extends Events
*
* @description Persists generic "state" to and reads it from the URL.
*/

import _ from 'lodash';
import rison from 'rison-node';
import applyDiff from 'ui/utils/diff_object';
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.
*/

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
10 changes: 10 additions & 0 deletions src/ui/public/vis/agg_configs.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@

/**
* @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
19 changes: 18 additions & 1 deletion src/ui/public/vis/vis.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@

/**
* @name Vis
*
* @description This class consists of aggs, params, listeners, title, and type.
* - Aggs: Instances of AggConfig.
* - Params: Are these the settings in the Options tab?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spalger Question

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep

*
* Not to be confused with vislib/vis.js.
*/

import _ from 'lodash';
import AggTypesIndexProvider from 'ui/agg_types/index';
import RegistryVisTypesProvider from 'ui/registry/vis_types';
Expand All @@ -24,7 +35,6 @@ export default function VisFactory(Notifier, Private) {

this.indexPattern = indexPattern;

// http://aphyr.com/data/posts/317/state.gif
this.setState(state);
this.setUiState(uiState);
}
Expand All @@ -36,6 +46,7 @@ export default function VisFactory(Notifier, Private) {

let schemas = type.schemas;

// NOTE: What is this doing?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spalger Question

Copy link
Contributor

@spalger spalger Aug 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, this was put in place to do migrations at runtime. I'm not certain when it went in, but I'm pretty sure we can remove it for 5.0

Copy link
Contributor

@spalger spalger Aug 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was around 4.0 that this went in, and it was only to support people who had saved visualizations during the 4.0 betas. I would be pretty surprised if someone expects visualizations created in the 4.0 betas to open fine in 5.0... but they might not realize how long it's been since they saved their visualizations, and we have done everything possible to avoid writing real migrations.

let aggs = _.transform(oldState, function (newConfigs, oldConfigs, oldGroupName) {
let schema = schemas.all.byName[oldGroupName];

Expand Down Expand Up @@ -119,6 +130,7 @@ export default function VisFactory(Notifier, Private) {
};

Vis.prototype.requesting = function () {
// Invoke requesting() on each agg. Aggs is an instance of AggConfigs.
_.invoke(this.aggs.getRequestAggs(), 'requesting');
};

Expand Down Expand Up @@ -149,6 +161,11 @@ export default function VisFactory(Notifier, Private) {
Vis.prototype.getUiState = function () {
return this.__uiState;
};

/**
* Currently this is only used to extract map-specific information
* (e.g. mapZoom, mapCenter).
*/
Vis.prototype.uiStateVal = function (key, val) {
if (this.hasUiState()) {
if (_.isUndefined(val)) {
Expand Down