kibana-123
Version:
Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic
279 lines (236 loc) • 7.39 kB
JavaScript
/**
* @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';
import applyDiff from 'ui/utils/diff_object';
import EventsProvider from 'ui/events';
import Notifier from 'ui/notify/notifier';
import {
createStateHash,
hashedItemStoreSingleton,
isStateHash,
} from './state_storage';
export default function StateProvider(Private, $rootScope, $location, config) {
const Events = Private(EventsProvider);
_.class(State).inherits(Events);
function State(
urlParam,
defaults,
hashedItemStore = hashedItemStoreSingleton,
notifier = new Notifier()
) {
State.Super.call(this);
this.setDefaults(defaults);
this._urlParam = urlParam || '_s';
this._notifier = notifier;
this._hashedItemStore = hashedItemStore;
// When the URL updates we need to fetch the values from the URL
this._cleanUpListeners = _.partial(_.callEach, [
// partial route update, no app reload
$rootScope.$on('$routeUpdate', () => {
this.fetch();
}),
// beginning of full route update, new app will be initialized before
// $routeChangeSuccess or $routeChangeError
$rootScope.$on('$routeChangeStart', () => {
if (!this._persistAcrossApps) {
this.destroy();
}
}),
$rootScope.$on('$routeChangeSuccess', () => {
if (this._persistAcrossApps) {
this.fetch();
}
})
]);
// Initialize the State with fetch
this.fetch();
}
State.prototype._readFromURL = function () {
const search = $location.search();
const urlVal = search[this._urlParam];
if (!urlVal) {
return null;
}
if (isStateHash(urlVal)) {
return this._parseQueryParamValue(urlVal);
}
let risonEncoded;
let unableToParse;
try {
risonEncoded = rison.decode(urlVal);
} catch (e) {
unableToParse = true;
}
if (unableToParse) {
this._notifier.error('Unable to parse URL');
search[this._urlParam] = this.toQueryParam(this._defaults);
$location.search(search).replace();
}
if (!risonEncoded) {
return null;
}
if (this.isHashingEnabled()) {
// RISON can find its way into the URL any number of ways, including the navbar links or
// shared urls with the entire state embedded. These values need to be translated into
// hashes and replaced in the browser history when state-hashing is enabled
search[this._urlParam] = this.toQueryParam(risonEncoded);
$location.search(search).replace();
}
return risonEncoded;
};
/**
* Fetches the state from the url
* @returns {void}
*/
State.prototype.fetch = function () {
let stash = this._readFromURL();
// nothing to read from the url? save if ordered to persist
if (stash === null) {
if (this._persistAcrossApps) {
return this.save();
} else {
stash = {};
}
}
_.defaults(stash, this._defaults);
// apply diff to state from stash, will change state in place via side effect
let diffResults = applyDiff(this, stash);
if (diffResults.keys.length) {
this.emit('fetch_with_changes', diffResults.keys);
}
};
/**
* Saves the state to the url
* @returns {void}
*/
State.prototype.save = function (replace) {
let stash = this._readFromURL();
let state = this.toObject();
replace = replace || false;
if (!stash) {
replace = true;
stash = {};
}
// apply diff to state from stash, will change state in place via side effect
let diffResults = applyDiff(stash, _.defaults({}, state, this._defaults));
if (diffResults.keys.length) {
this.emit('save_with_changes', diffResults.keys);
}
// persist the state in the URL
let search = $location.search();
search[this._urlParam] = this.toQueryParam(state);
if (replace) {
$location.search(search).replace();
} else {
$location.search(search);
}
};
/**
* Calls save with a forced replace
* @returns {void}
*/
State.prototype.replace = function () {
this.save(true);
};
/**
* Resets the state to the defaults
*
* @returns {void}
*/
State.prototype.reset = function () {
// apply diff to _attributes from defaults, this is side effecting so
// it will change the state in place.
let diffResults = applyDiff(this, this._defaults);
if (diffResults.keys.length) {
this.emit('reset_with_changes', diffResults.keys);
}
this.save();
};
/**
* Cleans up the state object
* @returns {void}
*/
State.prototype.destroy = function () {
this.off(); // removes all listeners
this._cleanUpListeners(); // Removes the $routeUpdate listener
};
State.prototype.setDefaults = function (defaults) {
this._defaults = defaults || {};
};
/**
* Parse the query param value to it's unserialized
* value. Hashes are restored to their pre-hashed state.
*
* @param {string} queryParam - value from the query string
* @return {any} - the stored value, or null if hash does not resolve
*/
State.prototype._parseQueryParamValue = function (queryParam) {
if (!isStateHash(queryParam)) {
return rison.decode(queryParam);
}
const json = this._hashedItemStore.getItem(queryParam);
if (json === null) {
this._notifier.error('Unable to completely restore the URL, be sure to use the share functionality.');
}
return JSON.parse(json);
};
/**
* Lookup the value for a hash and return it's value
* in rison format
*
* @param {string} hash
* @return {string} rison
*/
State.prototype.translateHashToRison = function (hash) {
return rison.encode(this._parseQueryParamValue(hash));
};
State.prototype.isHashingEnabled = function () {
return !!config.get('state:storeInSessionStorage');
};
/**
* Produce the hash version of the state in it's current position
*
* @return {string}
*/
State.prototype.toQueryParam = function (state = this.toObject()) {
if (!this.isHashingEnabled()) {
return rison.encode(state);
}
// We need to strip out Angular-specific properties.
const json = angular.toJson(state);
const hash = createStateHash(json, hash => {
return this._hashedItemStore.getItem(hash);
});
const isItemSet = this._hashedItemStore.setItem(hash, json);
if (isItemSet) {
return hash;
}
// If we ran out of space trying to persist the state, notify the user.
this._notifier.fatal(
new Error(
'Kibana is unable to store history items in your session ' +
'because it is full and there don\'t seem to be items any items safe ' +
'to delete.\n' +
'\n' +
'This can usually be fixed by moving to a fresh tab, but could ' +
'be caused by a larger issue. If you are seeing this message regularly, ' +
'please file an issue at https://github.com/elastic/kibana/issues.'
)
);
};
/**
* Get the query string parameter name where this state writes and reads
* @return {string}
*/
State.prototype.getQueryParamName = function () {
return this._urlParam;
};
return State;
};