From 34e3f498fcd2d610759741b275180bc0e16a8cec Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 9 Aug 2016 17:10:48 -0700 Subject: [PATCH] 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 --- .../kibana/public/visualize/editor/editor.js | 38 ++++++++++++-- .../saved_visualizations/_saved_vis.js | 9 ++++ .../courier/data_source/_doc_send_to_es.js | 7 +++ .../public/courier/data_source/doc_source.js | 10 ++++ .../courier/data_source/search_source.js | 52 +++++++++++++++++++ .../courier/saved_object/saved_object.js | 11 ++++ src/ui/public/es.js | 7 +++ src/ui/public/events.js | 6 +++ .../public/persisted_state/persisted_state.js | 8 ++- src/ui/public/state_management/app_state.js | 21 +++++++- src/ui/public/state_management/state.js | 8 +++ src/ui/public/vis/agg_config.js | 11 +++- src/ui/public/vis/agg_configs.js | 9 ++++ src/ui/public/vis/vis.js | 19 ++++++- 14 files changed, 206 insertions(+), 10 deletions(-) diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.js b/src/core_plugins/kibana/public/visualize/editor/editor.js index 548aebe4684b3..422550b3cca0f 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/core_plugins/kibana/public/visualize/editor/editor.js @@ -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 diff --git a/src/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index 20ab2138469b7..0bf171cd90333 100644 --- a/src/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -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) { diff --git a/src/ui/public/courier/data_source/_doc_send_to_es.js b/src/ui/public/courier/data_source/_doc_send_to_es.js index 5cc07f9e0425e..f7191b03100a9 100644 --- a/src/ui/public/courier/data_source/_doc_send_to_es.js +++ b/src/ui/public/courier/data_source/_doc_send_to_es.js @@ -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'; diff --git a/src/ui/public/courier/data_source/doc_source.js b/src/ui/public/courier/data_source/doc_source.js index 67d6de7e9e6b5..80be1cf7b6095 100644 --- a/src/ui/public/courier/data_source/doc_source.js +++ b/src/ui/public/courier/data_source/doc_source.js @@ -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'; diff --git a/src/ui/public/courier/data_source/search_source.js b/src/ui/public/courier/data_source/search_source.js index fa15f248c92a0..109cca094ca55 100644 --- a/src/ui/public/courier/data_source/search_source.js +++ b/src/ui/public/courier/data_source/search_source.js @@ -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'; diff --git a/src/ui/public/courier/saved_object/saved_object.js b/src/ui/public/courier/saved_object/saved_object.js index 5c32ccac9064c..8cfe0fe1bb39f 100644 --- a/src/ui/public/courier/saved_object/saved_object.js +++ b/src/ui/public/courier/saved_object/saved_object.js @@ -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'; diff --git a/src/ui/public/es.js b/src/ui/public/es.js index dfbceaeec2b45..def095dc08b41 100644 --- a/src/ui/public/es.js +++ b/src/ui/public/es.js @@ -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'; diff --git a/src/ui/public/events.js b/src/ui/public/events.js index 5f7a29ccf398b..1a6b5e39934dc 100644 --- a/src/ui/public/events.js +++ b/src/ui/public/events.js @@ -1,3 +1,9 @@ +/** + * @name Events + * + * @extends SimpleEmitter + */ + import _ from 'lodash'; import Notifier from 'ui/notify/notifier'; import SimpleEmitter from 'ui/utils/simple_emitter'; diff --git a/src/ui/public/persisted_state/persisted_state.js b/src/ui/public/persisted_state/persisted_state.js index 3975c20c24ea6..d883732fb3a08 100644 --- a/src/ui/public/persisted_state/persisted_state.js +++ b/src/ui/public/persisted_state/persisted_state.js @@ -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; -}; \ No newline at end of file +}; diff --git a/src/ui/public/state_management/app_state.js b/src/ui/public/state_management/app_state.js index c2312c5cdcffa..903071a723677 100644 --- a/src/ui/public/state_management/app_state.js +++ b/src/ui/public/state_management/app_state.js @@ -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); diff --git a/src/ui/public/state_management/state.js b/src/ui/public/state_management/state.js index 84f5f6128ca1b..5dc113ecc6faa 100644 --- a/src/ui/public/state_management/state.js +++ b/src/ui/public/state_management/state.js @@ -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'; diff --git a/src/ui/public/vis/agg_config.js b/src/ui/public/vis/agg_config.js index c1250db719b98..6ac5e9349fb05 100644 --- a/src/ui/public/vis/agg_config.js +++ b/src/ui/public/vis/agg_config.js @@ -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 * diff --git a/src/ui/public/vis/agg_configs.js b/src/ui/public/vis/agg_configs.js index ccacd7cafaefa..293cd2504a7fa 100644 --- a/src/ui/public/vis/agg_configs.js +++ b/src/ui/public/vis/agg_configs.js @@ -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'; diff --git a/src/ui/public/vis/vis.js b/src/ui/public/vis/vis.js index f350c4bdbfd50..365cc8163c4a3 100644 --- a/src/ui/public/vis/vis.js +++ b/src/ui/public/vis/vis.js @@ -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)) {