---------

**Commit 1:**
Add comments and inline docs for visualization saving and editing process.
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

* Original sha: 34e3f498fc
* Authored by CJ Cenizal <cj@cenizal.com> on 2016-08-10T00:10:48Z
This commit is contained in:
Elastic Jasper 2016-10-11 15:41:28 -04:00
parent 2af16396d2
commit bce53132bf
14 changed files with 206 additions and 10 deletions

View file

@ -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,
@ -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 = [{
@ -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) : {},
@ -124,12 +138,17 @@ 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({
@ -137,7 +156,7 @@ function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $tim
}));
}
return $state;
return appState;
}());
function init() {
@ -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');
@ -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

View file

@ -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) {

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -1,3 +1,9 @@
/**
* @name Events
*
* @extends SimpleEmitter
*/
import _ from 'lodash';
import Notifier from 'ui/notify/notifier';
import SimpleEmitter from 'ui/utils/simple_emitter';

View file

@ -1,3 +1,9 @@
/**
* @name PersistedState
*
* @extends Events
*/
import _ from 'lodash';
import toPath from 'lodash/internal/toPath';
import errors from 'ui/errors';
@ -269,4 +275,4 @@ export default function (Private) {
};
return PersistedState;
};
};

View file

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

View file

@ -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';

View file

@ -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) {
@ -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]
*/
@ -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
*

View file

@ -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';

View file

@ -1,3 +1,13 @@
/**
* @name Vis
*
* @description This class consists of aggs, params, listeners, title, and type.
* - Aggs: Instances of AggConfig.
* - Params: The settings in the Options tab.
*
* 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';
@ -24,7 +34,6 @@ export default function VisFactory(Notifier, Private) {
this.indexPattern = indexPattern;
// http://aphyr.com/data/posts/317/state.gif
this.setState(state);
this.setUiState(uiState);
}
@ -36,6 +45,8 @@ export default function VisFactory(Notifier, Private) {
let schemas = type.schemas;
// This was put in place to do migrations at runtime. It's used to support people who had saved
// visualizations during the 4.0 betas.
let aggs = _.transform(oldState, function (newConfigs, oldConfigs, oldGroupName) {
let schema = schemas.all.byName[oldGroupName];
@ -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');
};
@ -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)) {