Merge pull request #8624 from elastic/jasper/backport/8208/5.0

[backport] PR #8208 to 5.0
This commit is contained in:
CJ Cenizal 2016-10-11 12:42:08 -07:00 committed by GitHub
commit 772889f464
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)) {