Merge branch 'master' into preventHomeDirAccess

This commit is contained in:
spalger 2015-11-04 11:02:09 -06:00
commit c2cb8761a1
51 changed files with 1136 additions and 147 deletions

View file

@ -505,6 +505,13 @@ d3.selectAll('g.bar')
.each(function() ... )
```
```js
$http.get('/info')
.then(({ data }) => this.transfromInfo(data))
.then((transformed) => $http.post('/new-info', transformed))
.then(({ data }) => console.log(data));
```
*Wrong:*
```js
@ -519,6 +526,13 @@ d3.selectAll('g.bar')
.each(function() ... )
```
```js
$http.get('/info')
.then(({ data }) => this.transfromInfo(data))
.then((transformed) => $http.post('/new-info', transformed))
.then(({ data }) => console.log(data));
```
## Name your closures
Feel free to give your closures a descriptive name. It shows that you care about them, and
@ -867,7 +881,7 @@ When a node has multiple attributes that would cause it to exceed the line chara
attribute1="value1"
attribute2="value2"
attribute3="value3">
<li></li>
<li></li>
...

View file

@ -34,8 +34,8 @@ module.exports = function (program) {
.option('-e, --elasticsearch <uri>', 'Elasticsearch instance')
.option(
'-c, --config <path>',
'Path to the config file',
fromRoot('config/kibana.yml'))
'Path to the config file, can be changed with the CONFIG_PATH environment variable as well',
process.env.CONFIG_PATH || fromRoot('config/kibana.yml'))
.option('-p, --port <port>', 'The port to bind to', parseInt)
.option('-q, --quiet', 'Prevent all logging except errors')
.option('-Q, --silent', 'Prevent all logging')

View file

@ -19,7 +19,7 @@ module.exports = function (kibana) {
startupTimeout: Joi.number().default(5000),
ssl: Joi.object({
verify: Joi.boolean().default(true),
ca: Joi.string(),
ca: Joi.array().single().items(Joi.string()),
cert: Joi.string(),
key: Joi.string()
}).default(),

View file

@ -1,6 +1,6 @@
var url = require('url');
var _ = require('lodash');
var readFile = _.partialRight(require('fs').readFileSync, 'utf8');
var readFile = (file) => require('fs').readFileSync(file, 'utf8');
var http = require('http');
var https = require('https');
@ -14,8 +14,8 @@ module.exports = _.memoize(function (server) {
rejectUnauthorized: config.get('elasticsearch.ssl.verify')
};
if (config.get('elasticsearch.ssl.ca')) {
agentOptions.ca = [readFile(config.get('elasticsearch.ssl.ca'))];
if (_.size(config.get('elasticsearch.ssl.ca'))) {
agentOptions.ca = config.get('elasticsearch.ssl.ca').map(readFile);
}
// Add client certificate and key if required by elasticsearch
@ -29,4 +29,4 @@ module.exports = _.memoize(function (server) {
// See https://lodash.com/docs#memoize: We use a Map() instead of the default, because we want the keys in the cache
// to be the server objects, and by default these would be coerced to strings as keys (which wouldn't be useful)
module.exports.cache = new Map();
module.exports.cache = new Map();

View file

@ -1,6 +1,6 @@
var elasticsearch = require('elasticsearch');
var _ = require('lodash');
var fs = require('fs');
var readFile = (file) => require('fs').readFileSync(file, 'utf8');
var util = require('util');
var url = require('url');
var callWithRequest = require('./call_with_request');
@ -31,11 +31,11 @@ module.exports = function (server) {
var ssl = { rejectUnauthorized: options.verifySsl };
if (options.clientCrt && options.clientKey) {
ssl.cert = fs.readFileSync(options.clientCrt, 'utf8');
ssl.key = fs.readFileSync(options.clientKey, 'utf8');
ssl.cert = readFile(options.clientCrt);
ssl.key = readFile(options.clientKey);
}
if (options.ca) {
ssl.ca = fs.readFileSync(options.ca, 'utf8');
ssl.ca = options.ca.map(readFile);
}
return new elasticsearch.Client({

View file

@ -17,6 +17,7 @@ define(function (require) {
return {
savedObj: savedSearch,
panel: panel,
uiState: savedSearch.uiStateJSON ? JSON.parse(savedSearch.uiStateJSON) : {},
editUrl: savedSearches.urlFor(panel.id)
};
});

View file

@ -5,17 +5,18 @@ define(function (require) {
return function (panel, $scope) { // Function parameters here
return savedVisualizations.get(panel.id)
.then(function (savedVis) {
// $scope.state comes via $scope inheritence from the dashboard app. Don't love this.
savedVis.vis.listeners.click = filterBarClickHandler($scope.state);
savedVis.vis.listeners.brush = brushEvent;
.then(function (savedVis) {
// $scope.state comes via $scope inheritence from the dashboard app. Don't love this.
savedVis.vis.listeners.click = filterBarClickHandler($scope.state);
savedVis.vis.listeners.brush = brushEvent;
return {
savedObj: savedVis,
panel: panel,
editUrl: savedVisualizations.urlFor(panel.id)
};
});
return {
savedObj: savedVis,
panel: panel,
uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {},
editUrl: savedVisualizations.urlFor(panel.id)
};
});
};
};
});

View file

@ -28,6 +28,8 @@
<visualize ng-switch-when="visualization"
vis="savedObj.vis"
search-source="savedObj.searchSource"
show-spy-panel="chrome.getVisible()"
ui-state="uiState"
class="panel-content">
</visualize>

View file

@ -22,6 +22,10 @@ define(function (require) {
var brushEvent = Private(require('ui/utils/brush_event'));
var getPanelId = function (panel) {
return ['P', panel.panelIndex].join('-');
};
return {
restrict: 'E',
template: require('plugins/kibana/dashboard/components/panel/panel.html'),
@ -39,7 +43,14 @@ define(function (require) {
// These could be done in loadPanel, putting them here to make them more explicit
$scope.savedObj = panelConfig.savedObj;
$scope.editUrl = panelConfig.editUrl;
$scope.$on('$destroy', panelConfig.savedObj.destroy);
$scope.$on('$destroy', function () {
panelConfig.savedObj.destroy();
$scope.parentUiState.removeChild(getPanelId(panelConfig.panel));
});
// create child ui state from the savedObj
var uiState = panelConfig.uiState || {};
$scope.uiState = $scope.parentUiState.createChild(getPanelId(panelConfig.panel), uiState, true);
$scope.filter = function (field, value, operator) {
var index = $scope.savedObj.searchSource.get('index').id;

View file

@ -157,6 +157,7 @@ define(function (require) {
panel.$scope = $scope.$new();
panel.$scope.panel = panel;
panel.$scope.parentUiState = $scope.uiState;
panel.$el = $compile('<li><dashboard-panel></li>')(panel.$scope);

View file

@ -81,11 +81,14 @@ define(function (require) {
title: dash.title,
panels: dash.panelsJSON ? JSON.parse(dash.panelsJSON) : [],
options: dash.optionsJSON ? JSON.parse(dash.optionsJSON) : {},
uiState: dash.uiStateJSON ? JSON.parse(dash.uiStateJSON) : {},
query: extractQueryFromFilters(dash.searchSource.getOwn('filter')) || {query_string: {query: '*'}},
filters: _.reject(dash.searchSource.getOwn('filter'), matchQueryFilter),
};
var $state = $scope.state = new AppState(stateDefaults);
var $uiState = $scope.uiState = $state.makeStateful('uiState');
$scope.$watchCollection('state.options', function (newVal, oldVal) {
if (!angular.equals(newVal, oldVal)) $state.save();
});
@ -115,9 +118,30 @@ define(function (require) {
docTitle.change(dash.title);
}
initPanelIndices();
$scope.$emit('application.load');
}
function initPanelIndices() {
// find the largest panelIndex in all the panels
var maxIndex = getMaxPanelIndex();
// ensure that all panels have a panelIndex
$scope.state.panels.forEach(function (panel) {
if (!panel.panelIndex) {
panel.panelIndex = maxIndex++;
}
});
}
function getMaxPanelIndex() {
var index = $scope.state.panels.reduce(function (idx, panel) {
// if panel is missing an index, add one and increment the index
return Math.max(idx, panel.panelIndex || idx);
}, 0);
return ++index;
}
function updateQueryOnRootSource() {
var filters = queryFilter.getFilters();
if ($state.query) {
@ -157,7 +181,9 @@ define(function (require) {
$scope.save = function () {
$state.title = dash.id = dash.title;
$state.save();
dash.panelsJSON = angular.toJson($state.panels);
dash.uiStateJSON = angular.toJson($uiState.getChanges());
dash.timeFrom = dash.timeRestore ? timefilter.time.from : undefined;
dash.timeTo = dash.timeRestore ? timefilter.time.to : undefined;
dash.optionsJSON = angular.toJson($state.options);
@ -193,12 +219,12 @@ define(function (require) {
// called by the saved-object-finder when a user clicks a vis
$scope.addVis = function (hit) {
pendingVis++;
$state.panels.push({ id: hit.id, type: 'visualization' });
$state.panels.push({ id: hit.id, type: 'visualization', panelIndex: getMaxPanelIndex() });
};
$scope.addSearch = function (hit) {
pendingVis++;
$state.panels.push({ id: hit.id, type: 'search' });
$state.panels.push({ id: hit.id, type: 'search', panelIndex: getMaxPanelIndex() });
};
// Setup configurable values for config directive, after objects are initialized

View file

@ -11,7 +11,7 @@ define(function (require) {
_.class(SavedDashboard).inherits(courier.SavedObject);
function SavedDashboard(id) {
// Gives our SavedDashboard the properties of a SavedObject
courier.SavedObject.call(this, {
SavedDashboard.Super.call(this, {
type: SavedDashboard.type,
mapping: SavedDashboard.mapping,
searchSource: SavedDashboard.searchsource,
@ -28,6 +28,7 @@ define(function (require) {
optionsJSON: angular.toJson({
darkTheme: config.get('dashboard:defaultDarkTheme')
}),
uiStateJSON: '{}',
version: 1,
timeRestore: false,
timeTo: undefined,
@ -50,6 +51,7 @@ define(function (require) {
description: 'string',
panelsJSON: 'string',
optionsJSON: 'string',
uiStateJSON: 'string',
version: 'integer',
timeRestore: 'boolean',
timeTo: 'string',

View file

@ -26,7 +26,6 @@ define(function (require) {
// Returns a single dashboard by ID, should be the name of the dashboard
this.get = function (id) {
// Returns a promise that contains a dashboard which is a subclass of docSource
return (new SavedDashboard(id)).init();
};
@ -42,7 +41,6 @@ define(function (require) {
});
};
this.find = function (searchString, size = 100) {
var self = this;
var body;

View file

@ -10,7 +10,7 @@ var chrome = require('ui/chrome');
var routes = require('ui/routes');
var modules = require('ui/modules');
var kibanaLogoUrl = require('ui/images/kibana.png');
var kibanaLogoUrl = require('ui/images/kibana.svg');
routes
.otherwise({

View file

@ -126,8 +126,8 @@
<div class="vis-editor-content">
<div class="collapsible-sidebar">
<vis-editor-sidebar class="vis-editor-sidebar" ng-if="chrome.getVisible()"></vis-editor-sidebar>
<div class="collapsible-sidebar" ng-if="chrome.getVisible()">
<vis-editor-sidebar class="vis-editor-sidebar"></vis-editor-sidebar>
</div>
<div class="vis-editor-canvas" ng-class="{ embedded: !chrome.getVisible() }">
@ -138,7 +138,7 @@
</div>
</div>
<visualize vis="vis" editable-vis="editableVis" search-source="savedVis.searchSource"></visualize>
<visualize vis="vis" ui-state="uiState" show-spy-panel="chrome.getVisible()" search-source="savedVis.searchSource" editable-vis="editableVis"></visualize>
</div>
</div>

View file

@ -85,6 +85,7 @@ define(function (require) {
var $state = $scope.$state = (function initState() {
var savedVisState = vis.getState();
var stateDefaults = {
uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {},
linked: !!savedVis.savedSearchId,
query: searchSource.getOwn('query') || {query_string: {query: '*'}},
filters: searchSource.getOwn('filter') || [],
@ -114,6 +115,8 @@ define(function (require) {
$scope.indexPattern = vis.indexPattern;
$scope.editableVis = editableVis;
$scope.state = $state;
$scope.uiState = $state.makeStateful('uiState');
$scope.conf = _.pick($scope, 'doSave', 'savedVis', 'shareData');
$scope.configTemplate = configTemplate;
@ -210,7 +213,6 @@ define(function (require) {
}
};
$scope.startOver = function () {
kbnUrl.change('/visualize', {});
};
@ -218,6 +220,7 @@ define(function (require) {
$scope.doSave = function () {
savedVis.id = savedVis.title;
savedVis.visState = $state.vis;
savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges());
savedVis.save()
.then(function (id) {

View file

@ -30,6 +30,7 @@ define(function (require) {
def.type = opts.type;
return def;
}()),
uiStateJSON: '{}',
description: '',
savedSearchId: opts.savedSearchId,
version: 1
@ -44,6 +45,7 @@ define(function (require) {
SavedVis.mapping = {
title: 'string',
visState: 'json',
uiStateJSON: 'string',
description: 'string',
savedSearchId: 'string',
version: 'integer'

View file

@ -20,9 +20,11 @@ define(function (require) {
if (req && req.ms != null) stats.push(['Request Duration', req.ms + 'ms']);
if (resp && resp.hits) stats.push(['Hits', resp.hits.total]);
if (req.fetchParams.index) stats.push(['Index', req.fetchParams.index]);
if (req.fetchParams.type) stats.push(['Type', req.fetchParams.type]);
if (req.fetchParams.id) stats.push(['Id', req.fetchParams.id]);
if (req.fetchParams) {
if (req.fetchParams.index) stats.push(['Index', req.fetchParams.index]);
if (req.fetchParams.type) stats.push(['Type', req.fetchParams.type]);
if (req.fetchParams.id) stats.push(['Id', req.fetchParams.id]);
}
});
};

View file

@ -4,6 +4,7 @@ let _ = require('lodash');
let { zipObject } = require('lodash');
let override = require('./override');
let pkg = require('requirefrom')('src/utils')('packageJson');
const clone = require('./deepCloneWithBuffers');
const schema = Symbol('Joi Schema');
const schemaKeys = Symbol('Schema Extensions');
@ -15,7 +16,7 @@ module.exports = class Config {
this[schemaKeys] = new Map();
this[vals] = Object.create(null);
this[pendingSets] = new Map(_.pairs(_.cloneDeep(initialSettings || {})));
this[pendingSets] = new Map(_.pairs(clone(initialSettings || {})));
if (initialSchema) this.extendSchema(initialSchema);
}
@ -64,7 +65,7 @@ module.exports = class Config {
set(key, value) {
// clone and modify the config
let config = _.cloneDeep(this[vals]);
let config = clone(this[vals]);
if (_.isPlainObject(key)) {
config = override(config, key);
} else {
@ -114,7 +115,7 @@ module.exports = class Config {
get(key) {
if (!key) {
return _.cloneDeep(this[vals]);
return clone(this[vals]);
}
let value = _.get(this[vals], key);
@ -123,7 +124,7 @@ module.exports = class Config {
throw new Error('Unknown config key: ' + key);
}
}
return _.cloneDeep(value);
return clone(value);
}
has(key) {

View file

@ -0,0 +1,61 @@
import deepCloneWithBuffers from '../deepCloneWithBuffers';
import expect from 'expect.js';
describe('deepCloneWithBuffers()', function () {
it('deep clones objects', function () {
const source = {
a: {
b: {},
c: {},
d: [
{
e: 'f'
}
]
}
};
const output = deepCloneWithBuffers(source);
expect(source.a).to.eql(output.a);
expect(source.a).to.not.be(output.a);
expect(source.a.b).to.eql(output.a.b);
expect(source.a.b).to.not.be(output.a.b);
expect(source.a.c).to.eql(output.a.c);
expect(source.a.c).to.not.be(output.a.c);
expect(source.a.d).to.eql(output.a.d);
expect(source.a.d).to.not.be(output.a.d);
expect(source.a.d[0]).to.eql(output.a.d[0]);
expect(source.a.d[0]).to.not.be(output.a.d[0]);
});
it('copies buffers but keeps them buffers', function () {
const input = new Buffer('i am a teapot', 'utf8');
const output = deepCloneWithBuffers(input);
expect(Buffer.isBuffer(input)).to.be.ok();
expect(Buffer.isBuffer(output)).to.be.ok();
expect(Buffer.compare(output, input));
expect(output).to.not.be(input);
});
it('copies buffers that are deep', function () {
const input = {
a: {
b: {
c: new Buffer('i am a teapot', 'utf8')
}
}
};
const output = deepCloneWithBuffers(input);
expect(Buffer.isBuffer(input.a.b.c)).to.be.ok();
expect(Buffer.isBuffer(output.a.b.c)).to.be.ok();
expect(Buffer.compare(output.a.b.c, input.a.b.c));
expect(output.a.b.c).to.not.be(input.a.b.c);
});
});

View file

@ -0,0 +1,11 @@
import { cloneDeep } from 'lodash';
function cloneBuffersCustomizer(val) {
if (Buffer.isBuffer(val)) {
return new Buffer(val);
}
}
export default function (vals) {
return cloneDeep(vals, cloneBuffersCustomizer);
};

View file

@ -113,6 +113,10 @@ define(function (require) {
}),
description: 'Maps values to specified colors within visualizations'
},
'visualization:loadingDelay': {
value: '2s',
description: 'Time to wait before dimming visualizations during query'
},
'csv:separator': {
value: ',',
description: 'Separate exported values with this string',

View file

@ -0,0 +1,63 @@
describe('ui/courier/fetch/strategy/search', () => {
const _ = require('lodash');
const sinon = require('auto-release-sinon');
const expect = require('expect.js');
const ngMock = require('ngMock');
let Promise;
let $rootScope;
let search;
let reqsFetchParams;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject((Private, $injector) => {
Promise = $injector.get('Promise');
$rootScope = $injector.get('$rootScope');
search = Private(require('ui/courier/fetch/strategy/search'));
reqsFetchParams = [
{
index: ['logstash-123'],
type: 'blah',
search_type: 'blah2',
body: 'hm this is the body'
}
];
}));
describe('#clientMethod', () => {
it('is msearch', () => {
expect(search.clientMethod).to.equal('msearch');
});
});
describe('#reqsFetchParamsToBody()', () => {
context('when indexList is not empty', () => {
it('includes the index', () => {
let value;
search.reqsFetchParamsToBody(reqsFetchParams).then(val => value = val);
$rootScope.$apply();
expect(_.includes(value, '"index":["logstash-123"]')).to.be(true);
});
});
context('when indexList is empty', () => {
beforeEach(() => reqsFetchParams[0].index = []);
it('explicitly negates any indexes', () => {
let value;
search.reqsFetchParamsToBody(reqsFetchParams).then(val => value = val);
$rootScope.$apply();
expect(_.includes(value, '"index":["-*"]')).to.be(true);
});
});
});
describe('#getResponses()', () => {
it('returns the `responses` property of the given arg', () => {
const responses = [{}];
const returned = search.getResponses({ responses });
expect(returned).to.be(responses);
});
});
});

View file

@ -24,6 +24,17 @@ define(function (require) {
return indexList.toIndexList(timeBounds.min, timeBounds.max);
})
.then(function (indexList) {
// If we've reached this point and there are no indexes in the
// index list at all, it means that we shouldn't expect any indexes
// to contain the documents we're looking for, so we instead
// perform a request for an index pattern that we know will always
// return an empty result (ie. -*). If instead we had gone ahead
// with an msearch without any index patterns, elasticsearch would
// handle that request by querying *all* indexes, which is the
// opposite of what we want in this case.
if (_.isArray(indexList) && indexList.length === 0) {
indexList.push('-*');
}
return angular.toJson({
index: indexList,
type: fetchParams.type,

View file

@ -0,0 +1,57 @@
var angular = require('angular');
var expect = require('expect.js');
var ngMock = require('ngMock');
require('ui/directives/json_input');
describe('JSON input validation', function () {
var $compile;
var $rootScope;
var html = '<input ng-model="value" json-input require-keys=true />';
var element;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
beforeEach(function () {
element = $compile(html)($rootScope);
});
it('should be able to require keys', function () {
element.val('{}');
element.trigger('input');
expect(element.hasClass('ng-invalid')).to.be.ok();
});
it('should be able to not require keys', function () {
var html = '<input ng-model="value" json-input require-keys=false />';
var element = $compile(html)($rootScope);
element.val('{}');
element.trigger('input');
expect(element.hasClass('ng-valid')).to.be.ok();
});
it('should be able to read parse an input', function () {
element.val('{}');
element.trigger('input');
expect($rootScope.value).to.eql({});
});
it('should not allow invalid json', function () {
element.val('{foo}');
element.trigger('input');
expect(element.hasClass('ng-invalid')).to.be.ok();
});
it('should allow valid json', function () {
element.val('{"foo": "bar"}');
element.trigger('input');
expect($rootScope.value).to.eql({foo: 'bar'});
expect(element.hasClass('ng-valid')).to.be.ok();
});
});

View file

@ -0,0 +1,33 @@
define(function (require) {
var _ = require('lodash');
var angular = require('angular');
require('ui/modules')
.get('kibana')
.directive('jsonInput', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, $el, attrs, ngModelCntrl) {
ngModelCntrl.$formatters.push(toJSON);
ngModelCntrl.$parsers.push(fromJSON);
function fromJSON(value) {
try {
value = JSON.parse(value);
var validity = !scope.$eval(attrs.requireKeys) ? true : _.keys(value).length > 0;
ngModelCntrl.$setValidity('json', validity);
} catch (e) {
ngModelCntrl.$setValidity('json', false);
}
return value;
}
function toJSON(value) {
return angular.toJson(value, 2);
}
}
};
});
});

View file

@ -17,7 +17,7 @@ define(function (require) {
* Listens for events
* @param {string} name - The name of the event
* @param {function} handler - The function to call when the event is triggered
* @returns {undefined}
* @return {Events} - this, for chaining
*/
Events.prototype.on = function (name, handler) {
if (!_.isArray(this._listeners[name])) {
@ -46,15 +46,15 @@ define(function (require) {
* Removes an event listener
* @param {string} [name] - The name of the event
* @param {function} [handler] - The handler to remove
* @return {undefined}
* @return {Events} - this, for chaining
*/
Events.prototype.off = function (name, handler) {
if (!name && !handler) {
return this._listeners = {};
return this.removeAllListeners();
}
// exit early if there is not an event that matches
if (!this._listeners[name]) return;
if (!this._listeners[name]) return this;
// If no hander remove all the events
if (!handler) {

View file

@ -0,0 +1,85 @@
describe('update filters', function () {
var _ = require('lodash');
var sinon = require('auto-release-sinon');
var expect = require('expect.js');
var ngMock = require('ngMock');
var MockState = require('fixtures/mock_state');
var storeNames = {
app: 'appState',
global: 'globalState'
};
var queryFilter;
var appState;
var globalState;
var $rootScope;
beforeEach(ngMock.module(
'kibana',
'kibana/courier',
'kibana/global_state',
function ($provide) {
$provide.service('courier', require('fixtures/mock_courier'));
appState = new MockState({ filters: [] });
$provide.service('getAppState', function () {
return function () { return appState; };
});
globalState = new MockState({ filters: [] });
$provide.service('globalState', function () {
return globalState;
});
}
));
beforeEach(ngMock.inject(function (Private, _$rootScope_) {
$rootScope = _$rootScope_;
queryFilter = Private(require('ui/filter_bar/query_filter'));
}));
describe('updating', function () {
var currentFilter;
beforeEach(function () {
currentFilter = {query: { match: { extension: { query: 'jpg', type: 'phrase' } } } };
});
it('should be able to update a filter', function () {
var newFilter = _.cloneDeep(currentFilter);
newFilter.query.match.extension.query = 'png';
expect(currentFilter.query.match.extension.query).to.be('jpg');
queryFilter.updateFilter({
source: currentFilter,
model: newFilter
});
$rootScope.$digest();
expect(currentFilter.query.match.extension.query).to.be('png');
});
it('should replace the filter type if it is changed', function () {
var newFilter = {
'range': {
'bytes': {
'gte': 0,
'lt': 1000
}
}
};
expect(currentFilter.query).not.to.be(undefined);
queryFilter.updateFilter({
source: currentFilter,
model: newFilter,
type: 'query'
});
$rootScope.$digest();
expect(currentFilter.query).to.be(undefined);
expect(currentFilter.range).not.to.be(undefined);
expect(_.eq(currentFilter.range, newFilter.range)).to.be(true);
});
});
});

View file

@ -1,9 +1,9 @@
var angular = require('angular');
var _ = require('lodash');
var $ = require('jquery');
var ngMock = require('ngMock');
var expect = require('expect.js');
var sinon = require('sinon');
require('ui/filter_bar');
var MockState = require('fixtures/mock_state');
@ -17,6 +17,7 @@ describe('Filter Bar Directive', function () {
var queryFilter;
var mapFilter;
var $el;
var $scope;
// require('testUtils/noDigestPromises').activateForSuite();
beforeEach(ngMock.module('kibana/global_state', function ($provide) {
@ -59,6 +60,7 @@ describe('Filter Bar Directive', function () {
Promise.map(filters, mapFilter).then(function (filters) {
appState.filters = filters;
$el = $compile('<filter-bar></filter-bar>')($rootScope);
$scope = $el.isolateScope();
});
var off = $rootScope.$on('filterbar:updated', function () {
@ -83,5 +85,29 @@ describe('Filter Bar Directive', function () {
expect($(filters[3]).find('span')[0].innerHTML).to.equal('missing:');
expect($(filters[3]).find('span')[1].innerHTML).to.equal('"host"');
});
describe('editing filters', function () {
beforeEach(function () {
$scope.startEditingFilter(appState.filters[3]);
$scope.$digest();
});
it('should be able to edit a filter', function () {
expect($el.find('.filter-edit-container').length).to.be(1);
});
it('should be able to stop editing a filter', function () {
$scope.stopEditingFilter();
$scope.$digest();
expect($el.find('.filter-edit-container').length).to.be(0);
});
it('should merge changes after clicking done', function () {
sinon.spy($scope, 'updateFilter');
$scope.editDone();
expect($scope.updateFilter.called).to.be(true);
});
});
});
});

View file

@ -33,6 +33,7 @@ describe('Query Filter', function () {
expect(queryFilter.toggleAll).to.be.a('function');
expect(queryFilter.removeFilter).to.be.a('function');
expect(queryFilter.removeAll).to.be.a('function');
expect(queryFilter.updateFilter).to.be.a('function');
expect(queryFilter.invertFilter).to.be.a('function');
expect(queryFilter.invertAll).to.be.a('function');
expect(queryFilter.pinFilter).to.be.a('function');
@ -45,6 +46,7 @@ describe('Query Filter', function () {
require('./_getFilters');
require('./_addFilters');
require('./_removeFilters');
require('./_updateFilters');
require('./_toggleFilters');
require('./_invertFilters');
require('./_pinFilters');

View file

@ -32,8 +32,32 @@
<a class="action filter-remove" ng-click="removeFilter(filter)">
<i class="fa fa-fw fa-trash"></i>
</a>
<a class="action filter-edit" ng-click="startEditingFilter(filter)">
<i class="fa fa-fw fa-edit"></i>
</a>
</div>
</div>
<div class="filter-edit-container" ng-if="editingFilter">
<form role="form" name="editFilterForm" ng-submit="editDone()">
<div
json-input
require-keys="true"
ui-ace="{
mode: 'json',
onLoad: aceLoaded
}"
ng-model="editingFilter.model"></div>
<div class="form-group">
<button class="btn btn-primary" ng-click="stopEditingFilter()">Cancel</button>
<button type="submit" class="btn btn-success"
ng-disabled="editFilterForm.$invalid"
>Done</button>
<small ng-show="editFilterForm.$invalid">Could not parse JSON input</small>
</div>
</form>
</div>
<div class="filter-link">
<div class="filter-description small">
<a ng-click="showFilterActions = !showFilterActions">

View file

@ -3,6 +3,9 @@ define(function (require) {
var module = require('ui/modules').get('kibana');
var template = require('ui/filter_bar/filter_bar.html');
var moment = require('moment');
var angular = require('angular');
require('ui/directives/json_input');
module.directive('filterBar', function (Private, Promise, getAppState) {
var mapAndFlattenFilters = Private(require('ui/filter_bar/lib/mapAndFlattenFilters'));
@ -28,13 +31,21 @@ define(function (require) {
'invertFilter',
'invertAll',
'removeFilter',
'removeAll'
'removeAll',
'updateFilter'
].forEach(function (method) {
$scope[method] = queryFilter[method];
});
$scope.state = getAppState();
$scope.aceLoaded = function (editor) {
editor.$blockScrolling = Infinity;
var session = editor.getSession();
session.setTabSize(2);
session.setUseSoftTabs(true);
};
$scope.applyFilters = function (filters) {
// add new filters
$scope.addFilters(filterAppliedAndUnwrap(filters));
@ -46,6 +57,36 @@ define(function (require) {
}
};
var privateFieldRegexp = /(^\$|meta)/;
$scope.startEditingFilter = function (source) {
var model = _.cloneDeep(source);
var filterType;
//Hide private properties and figure out what type of filter this is
_.each(model, function (value, key) {
if (key.match(privateFieldRegexp)) {
delete model[key];
} else {
filterType = key;
}
});
$scope.editingFilter = {
source: source,
type: filterType,
model: model
};
};
$scope.stopEditingFilter = function () {
$scope.editingFilter = null;
};
$scope.editDone = function () {
$scope.updateFilter($scope.editingFilter);
$scope.stopEditingFilter();
};
$scope.clearFilterBar = function () {
$scope.newFilters = [];
$scope.changeTimeFilter = null;
@ -53,6 +94,7 @@ define(function (require) {
// update the scope filter list on filter changes
$scope.$listen(queryFilter, 'update', function () {
$scope.stopEditingFilter();
updateFilters();
});

View file

@ -28,7 +28,7 @@ filter-bar .confirm {
position: relative;
display: inline-block;
text-align: center;
min-width: 110px;
min-width: 140px;
font-size: @font-size-small;
background-color: @filter-bar-confirm-filter-bg;
@ -55,6 +55,11 @@ filter-bar .bar {
background: @filter-bar-bar-condensed-bg;
}
.ace_editor {
height: 175px;
margin: 15px 0;
}
.filter-link {
position: relative;
display: inline-block;
@ -72,7 +77,7 @@ filter-bar .bar {
position: relative;
display: inline-block;
text-align: center;
min-width: 110px;
min-width: 140px;
font-size: @font-size-small;
background-color: @filter-bar-bar-filter-bg;

View file

@ -21,8 +21,26 @@ describe('Filter Bar Directive', function () {
$rootScope.$apply();
});
it('should return undefined for none matching', function (done) {
var filter = { range: { gt: 0, lt: 1024 } };
it('should work with undefined filter types', function (done) {
var filter = {
'bool': {
'must': {
'term': {
'geo.src': 'US'
}
}
}
};
mapDefault(filter).then(function (result) {
expect(result).to.have.property('key', 'bool');
expect(result).to.have.property('value', JSON.stringify(filter.bool));
done();
});
$rootScope.$apply();
});
it('should return undefined if there is no valid key', function (done) {
var filter = { meta: {} };
mapDefault(filter).catch(function (result) {
expect(result).to.be(filter);
done();

View file

@ -74,7 +74,7 @@ describe('Filter Bar Directive', function () {
});
it('should finish with a catch', function (done) {
var before = { meta: { index: 'logstash-*' }, foo: '' };
var before = { meta: { index: 'logstash-*' }};
mapFilter(before).catch(function (error) {
expect(error).to.be.an(Error);
expect(error.message).to.be('No mappings have been found for filter.');

View file

@ -1,13 +1,17 @@
define(function (require) {
return function mapDefaultProvider(Promise) {
var angular = require('angular');
var _ = require('lodash');
var metaProperty = /(^\$|meta)/;
return function (filter) {
var key;
var value;
if (filter.query) {
key = 'query';
value = angular.toJson(filter.query);
var key = _.find(_.keys(filter), function (key) {
return !key.match(metaProperty);
});
if (key) {
var value = angular.toJson(filter[key]);
return Promise.resolve({ key: key, value: value });
}
return Promise.reject(filter);

View file

@ -8,6 +8,7 @@ define(function (require) {
var uniqFilters = require('ui/filter_bar/lib/uniqFilters');
var compareFilters = require('ui/filter_bar/lib/compareFilters');
var mapAndFlattenFilters = Private(require('ui/filter_bar/lib/mapAndFlattenFilters'));
var angular = require('angular');
var queryFilter = new EventEmitter();
@ -82,6 +83,26 @@ define(function (require) {
state.filters.splice(index, 1);
};
/**
* Updates an existing filter
* @param {object} filter Contains a reference to a filter and its new model
* @param {object} filter.source The filter reference
* @param {string} filter.model The edited filter
* @returns {object} Promise that resolves to the new filter on a successful merge
*/
queryFilter.updateFilter = function (filter) {
var mergedFilter = _.assign({}, filter.source, filter.model);
//If the filter type is changed we want to discard the old type
//when merging changes back in
var filterTypeReplaced = filter.model[filter.type] !== mergedFilter[filter.type];
if (filterTypeReplaced) {
delete mergedFilter[filter.type];
}
return angular.copy(mergedFilter, filter.source);
};
/**
* Removes all filters
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="252px"
height="45px"
viewBox="0 0 252 45"
enable-background="new 0 0 252 45"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="kibana.svg"><metadata
id="metadata4270"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs4268" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2046"
inkscape:window-height="1132"
id="namedview4266"
showgrid="false"
inkscape:zoom="1.0416667"
inkscape:cx="126"
inkscape:cy="22.5"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" /><font
horiz-adv-x="1000"
id="font4232"><!-- &quot;Helvetica Neue&quot; is a trademark of Heidelberger Druckmaschinen AG, which may be registered in certain jurisdictions, exclusivly licensed through Linotype Library GmbH, a wholly owned subsidiary of Heidelberger Druckmaschinen AG. --><font-face
font-family="HelveticaNeue-Bold"
units-per-em="1000"
underline-position="-100"
underline-thickness="50"
id="font-face4234" /><missing-glyph
horiz-adv-x="500"
d="M391,607l-280,0l0,-512l280,0M482,698l0,-695l-465,0l0,695z"
id="missing-glyph4236" /><glyph
unicode="a"
horiz-adv-x="574"
d="M48,358C50,391 58,419 73,441C88,463 106,481 129,494C152,507 177,517 206,523C234,528 262,531 291,531C317,531 343,529 370,526C397,522 421,515 443,504C465,493 483,479 497,460C511,441 518,415 518,384l0,-269C518,92 519,69 522,48C525,27 529,11 536,0l-144,0C389,8 387,16 386,25C384,33 383,41 382,50C359,27 333,10 302,1C271,-8 240,-13 208,-13C183,-13 160,-10 139,-4C118,2 99,11 83,24C67,37 55,53 46,72C37,91 32,114 32,141C32,170 37,195 48,214C58,233 71,248 88,259C104,270 123,279 144,285C165,290 186,295 207,298C228,301 249,304 270,306C291,308 309,311 325,315C341,319 354,325 363,333C372,340 377,351 376,366C376,381 374,394 369,403C364,412 357,419 349,424C340,429 331,432 320,434C309,435 297,436 284,436C256,436 234,430 218,418C202,406 193,386 190,358M376,253C370,248 363,244 354,241C345,238 335,235 325,233C314,231 303,229 292,228C281,227 269,225 258,223C247,221 237,218 227,215C216,212 207,207 200,202C192,196 186,189 181,180C176,171 174,160 174,147C174,134 176,124 181,115C186,106 192,100 200,95C208,90 217,86 228,84C239,82 250,81 261,81C289,81 311,86 326,95C341,104 353,116 360,129C367,142 372,155 374,168C375,181 376,192 376,200z"
id="glyph4238" /><glyph
unicode="b"
horiz-adv-x="611"
d="M433,258C433,237 431,216 426,196C421,176 414,158 404,143C394,128 382,116 367,107C352,98 333,93 312,93C291,93 273,98 258,107C243,116 230,128 220,143C210,158 203,176 198,196C193,216 191,237 191,258C191,280 193,301 198,321C203,341 210,359 220,374C230,389 243,402 258,411C273,420 291,424 312,424C333,424 352,420 367,411C382,402 394,389 404,374C414,359 421,341 426,321C431,301 433,280 433,258M54,714l0,-714l135,0l0,66l2,0C206,37 229,17 259,5C289,-7 323,-13 361,-13C387,-13 413,-8 438,3C463,14 486,30 507,52C527,74 543,102 556,137C569,171 575,212 575,259C575,306 569,347 556,382C543,416 527,444 507,466C486,488 463,504 438,515C413,526 387,531 361,531C329,531 298,525 269,513C239,500 215,481 198,454l-2,0l0,260z"
id="glyph4240" /><glyph
unicode="i"
horiz-adv-x="258"
d="M200,597l0,117l-142,0l0,-117M58,517l0,-517l142,0l0,517z"
id="glyph4242" /><glyph
unicode="k"
horiz-adv-x="574"
d="M67,714l0,-714l142,0l0,178l55,53l142,-231l172,0l-217,327l195,190l-168,0l-179,-186l0,383z"
id="glyph4244" /><glyph
unicode="n"
horiz-adv-x="593"
d="M54,517l0,-517l142,0l0,271C196,324 205,362 222,385C239,408 267,419 306,419C340,419 364,409 377,388C390,367 397,335 397,292l0,-292l142,0l0,318C539,350 536,379 531,406C525,432 515,454 501,473C487,491 468,505 444,516C419,526 388,531 350,531C320,531 291,524 262,511C233,497 210,475 192,445l-3,0l0,72z"
id="glyph4246" /></font><rect
fill="#3C3C3C"
width="252"
height="45"
id="rect4248"
style="opacity:1" /><rect
fill="#85C441"
width="6.094"
height="45"
id="rect4250"
style="opacity:1" /><rect
x="5.958"
fill="#2C448E"
width="6.094"
height="45"
id="rect4252"
style="opacity:1" /><rect
x="12.052"
fill="#F2BB1A"
width="9.847"
height="45"
id="rect4254"
style="opacity:1" /><rect
x="21.899"
fill="#3BBEB1"
width="6.068"
height="45"
id="rect4256"
style="opacity:1" /><rect
x="28.029"
fill="#006656"
width="2.84"
height="45"
id="rect4258"
style="opacity:1" /><rect
x="30.869"
fill="#EA458B"
width="15.006"
height="45"
id="rect4260"
style="opacity:1" /><rect
x="41.5"
fill="none"
width="207.5"
height="74"
id="rect4262" /><g
style="font-size:64.99947357px;font-family:HelveticaNeue-Bold;letter-spacing:-2.99203849px;opacity:1;fill:#ffffff"
id="text4264"><path
d="m 45.721,-0.49421886 0,46.40962386 9.229925,0 0,-11.569906 3.574971,-3.444972 9.229926,15.014878 11.179909,0 -14.104886,-21.254828 12.674898,-12.3499 -10.919912,0 -11.634906,12.089903 0,-24.89479886 -9.229925,0 z"
style="letter-spacing:-1.33094156px"
id="path4912" /><path
d="m 90.358907,7.1107196 0,-7.60493846 -9.229925,0 0,7.60493846 9.229925,0 z m -9.229925,5.1999574 0,33.604728 9.229925,0 0,-33.604728 -9.229925,0 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold';letter-spacing:-2.87854815px"
id="path4914" /><path
d="m 119.38291,29.145541 q 0,2.079983 -0.45499,4.029967 -0.455,1.949985 -1.42999,3.444973 -0.97499,1.494987 -2.46998,2.40498 -1.42999,0.844993 -3.50997,0.844993 -2.01499,0 -3.50998,-0.844993 -1.49498,-0.909993 -2.46998,-2.40498 -0.97499,-1.494988 -1.42998,-3.444973 -0.455,-1.949984 -0.455,-4.029967 0,-2.144983 0.455,-4.094967 0.45499,-1.949984 1.42998,-3.444972 0.975,-1.494988 2.46998,-2.339981 1.49499,-0.909993 3.50998,-0.909993 2.07998,0 3.50997,0.909993 1.49499,0.844993 2.46998,2.339981 0.97499,1.494988 1.42999,3.444972 0.45499,1.949984 0.45499,4.094967 z m -24.634798,-29.63975986 0,46.40962386 8.774928,0 0,-4.289965 0.13,0 q 1.49499,2.794977 4.41996,3.964968 2.92498,1.16999 6.62995,1.16999 2.53498,0 5.00496,-1.039991 2.46998,-1.039992 4.41996,-3.184974 2.01499,-2.144983 3.24998,-5.459956 1.23499,-3.379973 1.23499,-7.994935 0,-4.614963 -1.23499,-7.929936 -1.23499,-3.379973 -3.24998,-5.524955 -1.94998,-2.144983 -4.41996,-3.184975 -2.46998,-1.039991 -5.00496,-1.039991 -3.11997,0 -6.04495,1.23499 -2.85998,1.16999 -4.54996,3.769969 l -0.13,0 0,-16.89986286 -9.229928,0 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold';letter-spacing:-2.87854815px"
id="path4916" /><path
d="m 131.21557,22.645594 q 0.195,-3.249974 1.62499,-5.394957 1.42999,-2.144982 3.63997,-3.444972 2.20998,-1.299989 4.93996,-1.819985 2.79498,-0.584995 5.58995,-0.584995 2.53498,0 5.13496,0.389997 2.59998,0.324997 4.74496,1.364989 2.14499,1.039991 3.50997,2.924976 1.36499,1.819985 1.36499,4.87496 l 0,17.484859 q 0,2.274981 0.26,4.354965 0.26,2.079983 0.90999,3.119974 l -9.35992,0 q -0.26,-0.779993 -0.455,-1.559987 -0.13,-0.844993 -0.19499,-1.689986 -2.20999,2.274981 -5.19996,3.184974 -2.98998,0.909992 -6.10995,0.909992 -2.40498,0 -4.48497,-0.584995 -2.07998,-0.584995 -3.63997,-1.819985 -1.55998,-1.23499 -2.46998,-3.119975 -0.84499,-1.884985 -0.84499,-4.484964 0,-2.859976 0.97499,-4.679962 1.03999,-1.884984 2.59998,-2.989975 1.62499,-1.104991 3.63997,-1.624987 2.07998,-0.584995 4.15997,-0.909993 2.07998,-0.324997 4.09496,-0.519996 2.01499,-0.194998 3.57498,-0.584995 1.55998,-0.389997 2.46998,-1.104991 0.90999,-0.779994 0.84499,-2.209982 0,-1.494988 -0.52,-2.339981 -0.45499,-0.909993 -1.29999,-1.364989 -0.77999,-0.519996 -1.88498,-0.649995 -1.03999,-0.194998 -2.27498,-0.194998 -2.72998,0 -4.28997,1.16999 -1.55999,1.169991 -1.81998,3.899969 l -9.22993,0 z m 21.31983,6.824944 q -0.585,0.519996 -1.49499,0.844994 -0.84499,0.259998 -1.88498,0.454996 -0.975,0.194998 -2.07999,0.324997 -1.10499,0.129999 -2.20998,0.324998 -1.03999,0.194998 -2.07998,0.519995 -0.97499,0.324998 -1.75499,0.909993 -0.71499,0.519996 -1.16999,1.364989 -0.455,0.844993 -0.455,2.144983 0,1.23499 0.455,2.079983 0.455,0.844993 1.23499,1.364989 0.77999,0.454996 1.81999,0.649994 1.03999,0.194999 2.14498,0.194999 2.72998,0 4.22496,-0.909993 1.49499,-0.909992 2.20999,-2.144982 0.71499,-1.29999 0.84499,-2.599979 0.195,-1.29999 0.195,-2.079983 l 0,-3.444973 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold';letter-spacing:-2.87854815px"
id="path4918" /><path
d="m 165.92399,12.310677 0,33.604728 9.22992,0 0,-17.614857 q 0,-5.134958 1.68999,-7.344941 1.68999,-2.274981 5.45996,-2.274981 3.31497,0 4.61496,2.079983 1.29999,2.014984 1.29999,6.17495 l 0,18.979846 9.22992,0 0,-20.669832 q 0,-3.119975 -0.58499,-5.654955 -0.52,-2.599978 -1.88499,-4.354964 -1.36499,-1.819986 -3.76997,-2.794978 -2.33998,-1.039991 -6.04495,-1.039991 -2.92497,0 -5.71995,1.364989 -2.79498,1.299989 -4.54996,4.224966 l -0.195,0 0,-4.679963 -8.77493,0 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold';letter-spacing:-2.87854815px"
id="path4920" /><path
d="m 201.24887,22.645594 q 0.195,-3.249974 1.62499,-5.394957 1.42999,-2.144982 3.63997,-3.444972 2.20998,-1.299989 4.93996,-1.819985 2.79498,-0.584995 5.58996,-0.584995 2.53497,0 5.13495,0.389997 2.59998,0.324997 4.74497,1.364989 2.14498,1.039991 3.50997,2.924976 1.36499,1.819985 1.36499,4.87496 l 0,17.484859 q 0,2.274981 0.25999,4.354965 0.26,2.079983 0.91,3.119974 l -9.35993,0 q -0.26,-0.779993 -0.45499,-1.559987 -0.13,-0.844993 -0.195,-1.689986 -2.20998,2.274981 -5.19996,3.184974 -2.98998,0.909992 -6.10995,0.909992 -2.40498,0 -4.48496,-0.584995 -2.07999,-0.584995 -3.63998,-1.819985 -1.55998,-1.23499 -2.46998,-3.119975 -0.84499,-1.884985 -0.84499,-4.484964 0,-2.859976 0.97499,-4.679962 1.04,-1.884984 2.59998,-2.989975 1.62499,-1.104991 3.63997,-1.624987 2.07999,-0.584995 4.15997,-0.909993 2.07998,-0.324997 4.09497,-0.519996 2.01498,-0.194998 3.57497,-0.584995 1.55998,-0.389997 2.46998,-1.104991 0.90999,-0.779994 0.84499,-2.209982 0,-1.494988 -0.52,-2.339981 -0.45499,-0.909993 -1.29998,-1.364989 -0.78,-0.519996 -1.88499,-0.649995 -1.03999,-0.194998 -2.27498,-0.194998 -2.72998,0 -4.28997,1.16999 -1.55998,1.169991 -1.81998,3.899969 l -9.22993,0 z m 21.31983,6.824944 q -0.58499,0.519996 -1.49499,0.844994 -0.84499,0.259998 -1.88498,0.454996 -0.97499,0.194998 -2.07999,0.324997 -1.10499,0.129999 -2.20998,0.324998 -1.03999,0.194998 -2.07998,0.519995 -0.97499,0.324998 -1.75499,0.909993 -0.71499,0.519996 -1.16999,1.364989 -0.45499,0.844993 -0.45499,2.144983 0,1.23499 0.45499,2.079983 0.455,0.844993 1.23499,1.364989 0.78,0.454996 1.81999,0.649994 1.03999,0.194999 2.14498,0.194999 2.72998,0 4.22497,-0.909993 1.49498,-0.909992 2.20998,-2.144982 0.71499,-1.29999 0.84499,-2.599979 0.195,-1.29999 0.195,-2.079983 l 0,-3.444973 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold';letter-spacing:-2.87854815px"
id="path4922" /></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -50,21 +50,21 @@ describe('ui/index_patterns/_calculate_indices', () => {
context('when given start', () => {
beforeEach(() => run({ start: '1234567890' }));
it('includes min_value', () => {
expect(constraints['@something']).to.have.property('min_value');
it('includes max_value', () => {
expect(constraints['@something']).to.have.property('max_value');
});
it('min_value is gte', () => {
expect(constraints['@something'].min_value).to.have.property('gte');
it('max_value is gte', () => {
expect(constraints['@something'].max_value).to.have.property('gte');
});
});
context('when given stop', () => {
beforeEach(() => run({ stop: '1234567890' }));
it('includes max_value', () => {
expect(constraints['@something']).to.have.property('max_value');
it('includes min_value', () => {
expect(constraints['@something']).to.have.property('min_value');
});
it('max_value is lt', () => {
expect(constraints['@something'].max_value).to.have.property('lt');
it('min_value is lte', () => {
expect(constraints['@something'].min_value).to.have.property('lte');
});
});
});

View file

@ -17,10 +17,10 @@ define(function (require) {
function compileOptions(pattern, timeFieldName, start, stop) {
const constraints = {};
if (start) {
constraints.min_value = { gte: moment(start).valueOf() };
constraints.max_value = { gte: moment(start).valueOf() };
}
if (stop) {
constraints.max_value = { lt: moment(stop).valueOf() };
constraints.min_value = { lte: moment(stop).valueOf() };
}
return {

View file

@ -20,7 +20,7 @@ define(function (require) {
fields: [],
body: {
query: { match_all: {} },
size: 2147483647
size: 10000
}
})
.then(function (resp) {

View file

@ -192,6 +192,125 @@ describe('Persisted State', function () {
});
});
describe('child state removal', function () {
it('should clear path from parent state', function () {
var persistedState = new PersistedState();
var childState = persistedState.createChild('child', { userId: 1234 });
expect(persistedState.get()).to.eql({ child: { userId: 1234 }});
persistedState.removeChild('child');
expect(persistedState.get()).to.eql({});
});
it('should reset original parent value at path', function () {
var persistedState = new PersistedState({ user: 1234 });
var childState = persistedState.createChild('user', { id: 5678 });
expect(persistedState.get()).to.eql({ user: { id: 5678 }});
persistedState.removeChild('user');
expect(persistedState.get()).to.eql({ user: 1234 });
});
it('should clear changedState', function () {
var persistedState = new PersistedState({ user: 1234 });
var childState = persistedState.createChild('user');
childState.set('name', 'user name');
expect(persistedState.getChanges()).to.eql({ user: { name: 'user name' }});
persistedState.removeChild('user');
expect(persistedState.getChanges()).to.eql({});
});
});
describe('deep child state removal', function () {
it('should clear path from parent state', function () {
var persistedState = new PersistedState();
var childState = persistedState.createChild('child.state', { userId: 1234 });
expect(persistedState.get()).to.eql({ child: { state: { userId: 1234 }}});
persistedState.removeChild('child.state');
expect(persistedState.get()).to.eql({});
});
it('should reset original parent value at path', function () {
var persistedState = new PersistedState({ user: { id: 1234 }});
var childState = persistedState.createChild('user.id', 5678);
expect(persistedState.get()).to.eql({ user: { id: 5678 }});
persistedState.removeChild('user.id');
expect(persistedState.get()).to.eql({ user: { id: 1234 }});
});
it('should reset original parent other values at path', function () {
var persistedState = new PersistedState({ user: { name: 'user' }});
var childState = persistedState.createChild('user.id', 5678);
expect(persistedState.get()).to.eql({ user: { name: 'user', id: 5678 }});
persistedState.removeChild('user.id');
expect(persistedState.get()).to.eql({ user: { name: 'user' }});
});
it('should clear the changed state', function () {
var persistedState = new PersistedState({ user: { id: 1234 }});
var childState = persistedState.createChild('user.name');
childState.set('user name');
expect(persistedState.getChanges()).to.eql({ user: { name: 'user name' }});
persistedState.removeChild('user.name');
expect(persistedState.getChanges()).to.eql({});
});
});
describe('child state conditions', function () {
it('should be merged with the parent state', function () {
var parent = new PersistedState({ name: 'test' });
var child = parent.createChild('child', 'value');
expect(parent.get()).to.eql({
name: 'test',
child: 'value'
});
parent.set('id', 1234);
expect(parent.get()).to.eql({
id: 1234,
name: 'test',
child: 'value'
});
parent.set({});
expect(parent.get()).to.eql({
child: 'value'
});
});
it('should give child state precedence', function () {
var parent = new PersistedState({ user: { id: 1234, name: 'test' }});
var child = parent.createChild('user', { name: 'child test' });
expect(parent.get()).to.eql({
user: {
id: 1234,
name: 'child test'
}
});
parent.set({});
expect(parent.get()).to.eql({ user: { name: 'child test' }});
});
it('should be cleaned up with removeChild', function () {
var parent = new PersistedState({ name: 'test' });
var child = parent.createChild('child', 'value');
expect(parent.get()).to.eql({
name: 'test',
child: 'value'
});
parent.removeChild('child');
expect(parent.get()).to.eql({
name: 'test'
});
});
});
describe('colliding child paths and parent state values', function () {
it('should not change the child path value by default', function () {
var childIndex = 'childTest';
@ -226,7 +345,7 @@ describe('Persisted State', function () {
// pass in child state value
var childState = persistedState.createChild(childIndex, childStateValue);
// parent's default state overrides child state
// parent's default state is merged with child state
var compare = _.merge({}, childStateValue, persistedStateValue[childIndex]);
expect(childState.get()).to.eql(compare);
state = persistedState.get();
@ -299,7 +418,7 @@ describe('Persisted State', function () {
});
describe('get state', function () {
it('should perform deep gets with vairous formats', function () {
it('should perform deep gets with various formats', function () {
var obj = {
red: {
green: {
@ -336,6 +455,15 @@ describe('Persisted State', function () {
expect(persistedState.get('hello')).to.eql({ nouns: ['world', 'humans', 'everyone'] });
expect(persistedState.get('hello.nouns')).to.eql(['world', 'humans', 'everyone']);
});
it('should pass defaults to parent delegation', function () {
var persistedState = new PersistedState({ parent: true });
var childState = persistedState.createChild('child', { account: { name: 'first child' }});
var defaultValue = 'i have no data';
expect(childState.get('account.name', defaultValue)).to.eql('first child');
expect(childState.get('account.age', defaultValue)).to.eql(defaultValue);
});
});
describe('set state', function () {
@ -471,12 +599,24 @@ describe('Persisted State', function () {
expect(getByType('set')).to.have.length(1);
});
it('should not emit when setting value silently', function () {
expect(getByType('set')).to.have.length(0);
persistedState.setSilent('checker.time', 'now');
expect(getByType('set')).to.have.length(0);
});
it('should emit change when changing values', function () {
expect(getByType('change')).to.have.length(0);
persistedState.set('checker.time', 'now');
expect(getByType('change')).to.have.length(1);
});
it('should not emit when changing values silently', function () {
expect(getByType('change')).to.have.length(0);
persistedState.setSilent('checker.time', 'now');
expect(getByType('change')).to.have.length(0);
});
it('should not emit change when values are identical', function () {
expect(getByType('change')).to.have.length(0);
// check both forms of setting the same value
@ -512,6 +652,12 @@ describe('Persisted State', function () {
expect(getByType('change')).to.have.length(1);
});
it('should not emit when createChild set to silent', function () {
expect(getByType('change')).to.have.length(0);
persistedState.createChild('checker', { events: 'changed via child' }, true);
expect(getByType('change')).to.have.length(0);
});
it('should emit change when createChild adds new value', function () {
expect(getByType('change')).to.have.length(0);
persistedState.createChild('new.path', { another: 'thing' });

View file

@ -22,6 +22,21 @@ define(function (require) {
if (!_.isPlainObject(value)) throw new errors.PersistedStateError(msg);
}
function prepSetParams(key, value, path) {
// key must be the value, set the entire state using it
if (_.isUndefined(value) && (_.isPlainObject(key) || path.length > 0)) {
// setting entire tree, swap the key and value to write to the state
value = key;
key = undefined;
}
// ensure the value being passed in is never mutated
return {
value: _.cloneDeep(value),
key: key
};
}
function parentDelegationMixin(from, to) {
_.forOwn(from.prototype, function (method, methodName) {
to.prototype[methodName] = function () {
@ -34,7 +49,7 @@ define(function (require) {
parentDelegationMixin(SimpleEmitter, PersistedState);
parentDelegationMixin(Events, PersistedState);
function PersistedState(value, path, parent) {
function PersistedState(value, path, parent, silent) {
PersistedState.Super.call(this);
this._path = this._setPath(path);
@ -49,7 +64,7 @@ define(function (require) {
value = value || this._getDefault();
// copy passed state values and create internal trackers
this.set(value);
(silent) ? this.setSilent(value) : this.set(value);
this._initialized = true; // used to track state changes
}
@ -58,31 +73,48 @@ define(function (require) {
};
PersistedState.prototype.set = function (key, value) {
// key must be the value, set the entire state using it
if (_.isUndefined(value) && _.isPlainObject(key)) {
// swap the key and value to write to the state
value = key;
key = undefined;
}
// ensure the value being passed in is never mutated
value = _.cloneDeep(value);
var val = this._set(key, value);
var params = prepSetParams(key, value, this._path);
var val = this._set(params.key, params.value);
this.emit('set');
return val;
};
PersistedState.prototype.reset = function (key) {
this.set(key, undefined);
PersistedState.prototype.setSilent = function (key, value) {
var params = prepSetParams(key, value, this._path);
return this._set(params.key, params.value, true);
};
PersistedState.prototype.clear = function (key) {
this.set(key, null);
PersistedState.prototype.reset = function (path) {
var keyPath = this._getIndex(path);
var origValue = _.get(this._defaultState, keyPath);
var currentValue = _.get(this._mergedState, keyPath);
if (_.isUndefined(origValue)) {
this._cleanPath(path, this._mergedState);
} else {
_.set(this._mergedState, keyPath, origValue);
}
// clean up the changedState and defaultChildState trees
this._cleanPath(path, this._changedState);
this._cleanPath(path, this._defaultChildState);
if (!_.isEqual(currentValue, origValue)) this.emit('change');
};
PersistedState.prototype.createChild = function (path, value) {
return new PersistedState(value, this._getIndex(path), this._parent || this);
PersistedState.prototype.createChild = function (path, value, silent) {
this._setChild(this._getIndex(path), value, this._parent || this);
return new PersistedState(value, this._getIndex(path), this._parent || this, silent);
};
PersistedState.prototype.removeChild = function (path) {
var origValue = _.get(this._defaultState, this._getIndex(path));
if (_.isUndefined(origValue)) {
this.reset(path);
} else {
this.set(path, origValue);
}
};
PersistedState.prototype.getChanges = function () {
@ -106,6 +138,29 @@ define(function (require) {
return (this._path || []).concat(toPath(key));
};
PersistedState.prototype._getPartialIndex = function (key) {
var keyPath = this._getIndex(key);
return keyPath.slice(this._path.length);
};
PersistedState.prototype._cleanPath = function (path, stateTree) {
var partialPath = this._getPartialIndex(path);
var remove = true;
// recursively delete value tree, when no other keys exist
while (partialPath.length > 0) {
var lastKey = partialPath.splice(partialPath.length - 1, 1)[0];
var statePath = this._path.concat(partialPath);
var stateVal = statePath.length > 0 ? _.get(stateTree, statePath) : stateTree;
// if stateVal isn't an object, do nothing
if (!_.isPlainObject(stateVal)) return;
if (remove) delete stateVal[lastKey];
if (Object.keys(stateVal).length > 0) remove = false;
}
};
PersistedState.prototype._getDefault = function () {
var def = (this._hasPath()) ? undefined : {};
return (this._parent ? this.get() : def);
@ -119,13 +174,18 @@ define(function (require) {
return (isString) ? [this._getIndex(path)] : path;
};
PersistedState.prototype._setChild = function (path, value, parent) {
parent._defaultChildState = parent._defaultChildState || {};
_.set(parent._defaultChildState, path, value);
};
PersistedState.prototype._hasPath = function () {
return this._path.length > 0;
};
PersistedState.prototype._get = function (key, def) {
// delegate to parent instance
if (this._parent) return this._parent._get(this._getIndex(key), key);
if (this._parent) return this._parent._get(this._getIndex(key), def);
// no path and no key, get the whole state
if (!this._hasPath() && _.isUndefined(key)) {
@ -135,11 +195,12 @@ define(function (require) {
return _.get(this._mergedState, this._getIndex(key), def);
};
PersistedState.prototype._set = function (key, value, initialChildState, defaultChildState) {
PersistedState.prototype._set = function (key, value, silent, initialChildState) {
var self = this;
var stateChanged = false;
var initialState = !this._initialized;
var keyPath = this._getIndex(key);
var hasKeyPath = keyPath.length > 0;
// if this is the initial state value, save value as the default
if (initialState) {
@ -150,43 +211,59 @@ define(function (require) {
// delegate to parent instance, passing child's default value
if (this._parent) {
return this._parent._set(keyPath, value, initialState, this._defaultState);
return this._parent._set(keyPath, value, silent, initialState);
}
// everything in here affects only the parent state
if (!initialState) {
// no path and no key, set the whole state
if (!this._hasPath() && _.isUndefined(key)) {
// check for changes and emit an event when found
// compare changedState and new state, emit an event when different
stateChanged = !_.isEqual(this._changedState, value);
if (!initialChildState) this._changedState = value;
} else {
// check for changes and emit an event when found
stateChanged = !_.isEqual(this.get(keyPath), value);
// arrays merge by index, not the desired behavior - ensure they are replaced
if (!initialChildState) {
this._changedState = value;
this._mergedState = _.cloneDeep(value);
}
} else {
// check for changes at path, emit an event when different
var curVal = hasKeyPath ? this.get(keyPath) : this._mergedState;
stateChanged = !_.isEqual(curVal, value);
if (!initialChildState) {
// arrays are merge by index, not desired - ensure they are replaced
if (_.isArray(_.get(this._mergedState, keyPath))) {
_.set(this._mergedState, keyPath, undefined);
if (hasKeyPath) _.set(this._mergedState, keyPath, undefined);
else this._mergedState = undefined;
}
_.set(this._changedState, keyPath, value);
if (hasKeyPath) _.set(this._changedState, keyPath, value);
else this._changedState = _.isPlainObject(value) ? value : {};
}
}
}
// update the merged state value
var targetObj = this._mergedState || _.cloneDeep(this._defaultState);
var sourceObj = _.merge({}, defaultChildState, this._changedState);
var sourceObj = _.merge({}, this._defaultChildState, this._changedState);
// handler arguments are (targetValue, sourceValue, key, target, source)
var mergeMethod = function (targetValue, sourceValue, mergeKey) {
// If `mergeMethod` is provided it is invoked to produce the merged values of the destination and
// source properties. If `mergeMethod` returns `undefined` merging is handled by the method instead
// handler arguments are (targetValue, sourceValue, key, target, source)
// if not initial state, skip default merge method (ie. return value, see note below)
if (!initialState && !initialChildState && _.isEqual(keyPath, self._getIndex(mergeKey))) {
// use the sourceValue or fall back to targetValue
return !_.isUndefined(sourceValue) ? sourceValue : targetValue;
}
};
// If `mergeMethod` is provided it is invoked to produce the merged values of the
// destination and source properties.
// If `mergeMethod` returns `undefined` the default merging method is used
this._mergedState = _.merge(targetObj, sourceObj, mergeMethod);
if (stateChanged) this.emit('change');
// sanity check; verify that there are actually changes
if (_.isEqual(this._mergedState, this._defaultState)) this._changedState = {};
if (!silent && stateChanged) this.emit('change');
return this;
};

View file

@ -3,13 +3,17 @@ define(function (require) {
var modules = require('ui/modules');
var urlParam = '_a';
function AppStateProvider(Private, $rootScope, getAppState) {
var State = Private(require('ui/state_management/state'));
var PersistedState = Private(require('ui/persisted_state/persisted_state'));
var persistedStates;
var eventUnsubscribers;
_.class(AppState).inherits(State);
function AppState(defaults) {
persistedStates = {};
eventUnsubscribers = [];
AppState.Super.call(this, urlParam, defaults);
getAppState._set(this);
}
@ -17,10 +21,46 @@ define(function (require) {
// if the url param is missing, write it back
AppState.prototype._persistAcrossApps = false;
AppState.prototype.destroy = function () {
AppState.Super.prototype.destroy.call(this);
getAppState._set(null);
_.callEach(eventUnsubscribers);
};
AppState.prototype.makeStateful = function (prop) {
if (persistedStates[prop]) return persistedStates[prop];
var self = this;
// set up the ui state
persistedStates[prop] = new PersistedState();
// update the app state when the stateful instance changes
var updateOnChange = function () {
var replaceState = false; // TODO: debouncing logic
self[prop] = persistedStates[prop].getChanges();
self.save(replaceState);
};
var handlerOnChange = (method) => persistedStates[prop][method]('change', updateOnChange);
handlerOnChange('on');
eventUnsubscribers.push(() => handlerOnChange('off'));
// update the stateful object when the app state changes
var persistOnChange = function (changes) {
if (!changes) return;
if (changes.indexOf(prop) !== -1) {
persistedStates[prop].set(self[prop]);
}
};
var handlePersist = (method) => this[method]('fetch_with_changes', persistOnChange);
handlePersist('on');
eventUnsubscribers.push(() => handlePersist('off'));
// if the thing we're making stateful has an appState value, write to persisted state
if (self[prop]) persistedStates[prop].setSilent(self[prop]);
return persistedStates[prop];
};
return AppState;

View file

@ -1,16 +1,16 @@
<div ng-click="toggleDisplay()" class="visualize-show-spy">
<div class="visualize-show-spy-tab">
<i class="fa" ng-class="spy.mode ? 'fa-chevron-down' : 'fa-chevron-up'"></i>
<i class="fa" ng-class="spy.mode.name ? 'fa-chevron-down' : 'fa-chevron-up'"></i>
</div>
</div>
<div ng-show="spy.mode" class="visualize-spy-container">
<div ng-show="spy.mode.name" class="visualize-spy-container">
<header class="control-group">
<div class="fill visualize-spy-nav">
<a
class="btn btn-default"
ng-repeat="mode in modes.inOrder"
ng-class="{ active: spy.mode.name === mode.name }"
ng-click="setSpyMode(mode)">
ng-click="setSpyMode(mode.name)">
{{mode.display}}
</a>
</div>

View file

@ -5,57 +5,93 @@ define(function (require) {
var $ = require('jquery');
var _ = require('lodash');
var modes = Private(require('ui/registry/spy_modes'));
var defaultMode = modes.inOrder[0];
var spyModes = Private(require('ui/registry/spy_modes'));
var defaultMode = spyModes.inOrder[0].name;
return {
restrict: 'E',
template: require('ui/visualize/spy.html'),
link: function ($scope, $el) {
var currentSpy;
var $container = $el.find('.visualize-spy-container');
var fullPageSpy = false;
$scope.modes = modes;
var fullPageSpy = _.get($scope.spy, 'mode.fill', false);
$scope.modes = spyModes;
$scope.toggleDisplay = function () {
$scope.setSpyMode($scope.spy.mode ? null : defaultMode);
};
function getSpyObject(name) {
name = _.isUndefined(name) ? $scope.spy.mode.name : name;
fullPageSpy = (_.isNull(name)) ? false : fullPageSpy;
$scope.toggleFullPage = function () {
fullPageSpy = $scope.spy.mode.fill = !fullPageSpy;
};
return {
name: name,
fill: fullPageSpy,
};
}
$scope.setSpyMode = function (newMode) {
// allow passing in a mode name
if (_.isString(newMode)) newMode = modes.byName[newMode];
var current = $scope.spy.mode;
var change = false;
// no change
if (current && newMode && newMode.name === current.name) return;
var renderSpy = function (spyName) {
var newMode = $scope.modes.byName[spyName];
// clear the current value
if (current) {
current.$container.remove();
current.$scope.$destroy();
delete $scope.spy.mode;
if (currentSpy) {
currentSpy.$container && currentSpy.$container.remove();
currentSpy.$scope && currentSpy.$scope.$destroy();
$scope.spy.mode = {};
currentSpy = null;
}
// no further changes
if (!newMode) return;
current = $scope.spy.mode = {
// copy a couple values over
name: newMode.name,
display: newMode.display,
fill: fullPageSpy,
// update the spy mode and append to the container
$scope.spy.mode = getSpyObject(newMode.name);
currentSpy = _.assign({
$scope: $scope.$new(),
$container: $('<div class="visualize-spy-content">').appendTo($container)
};
}, $scope.spy.mode);
current.$container.append($compile(newMode.template)(current.$scope));
newMode.link && newMode.link(current.$scope, current.$container);
currentSpy.$container.append($compile(newMode.template)(currentSpy.$scope));
newMode.link && newMode.link(currentSpy.$scope, currentSpy.$container);
};
$scope.toggleDisplay = function () {
var modeName = _.get($scope.spy, 'mode.name');
$scope.setSpyMode(modeName ? null : defaultMode);
};
$scope.toggleFullPage = function () {
fullPageSpy = !fullPageSpy;
$scope.spy.mode = getSpyObject();
};
$scope.setSpyMode = function (modeName) {
// save the spy mode to the UI state
if (!_.isString(modeName)) modeName = null;
$scope.spy.mode = getSpyObject(modeName);
};
if ($scope.uiState) {
// sync external uiState changes
var syncUIState = () => $scope.spy.mode = $scope.uiState.get('spy.mode');
$scope.uiState.on('change', syncUIState);
$scope.$on('$destroy', () => $scope.uiState.off('change', syncUIState));
}
// re-render the spy when the name of fill modes change
$scope.$watchMulti([
'spy.mode.name',
'spy.mode.fill'
], function (newVals, oldVals) {
// update the ui state, but only if it really changes
var changedVals = newVals.filter((val) => !_.isUndefined(val)).length > 0;
if (changedVals && !_.isEqual(newVals, oldVals)) {
if ($scope.uiState) $scope.uiState.set('spy.mode', $scope.spy.mode);
}
// ensure the fill mode is synced
fullPageSpy = _.get($scope.spy, 'mode.fill', fullPageSpy);
renderSpy(_.get($scope.spy, 'mode.name', null));
});
}
};
});

View file

@ -8,7 +8,8 @@
<div class="item bottom"></div>
</div>
<div ng-hide="vis.type.requiresSearch && esResp.hits.total === 0"
ng-style="loadingStyle"
ng-class="{ loading: vis.type.requiresSearch && searchSource.activeFetchCount > 0 }"
class="visualize-chart"></div>
<!-- <pre>{{chartData | json}}</pre> -->
<visualize-spy ng-if="vis.type.requiresSearch"></visualize-spy>
<visualize-spy ng-if="vis.type.requiresSearch && showSpyPanel"></visualize-spy>

View file

@ -1,7 +1,7 @@
define(function (require) {
require('ui/modules')
.get('kibana/directive')
.directive('visualize', function (Notifier, SavedVis, indexPatterns, Private) {
.directive('visualize', function (Notifier, SavedVis, indexPatterns, Private, config) {
require('ui/visualize/spy');
require('ui/visualize/visualize.less');
@ -16,16 +16,22 @@ define(function (require) {
return {
restrict: 'E',
scope : {
showSpyPanel: '=?',
vis: '=',
uiState: '=?',
searchSource: '=?',
editableVis: '=?',
esResp: '=?',
searchSource: '=?'
},
template: require('ui/visualize/visualize.html'),
link: function ($scope, $el, attr) {
var chart; // set in "vis" watcher
var minVisChartHeight = 180;
if (_.isUndefined($scope.showSpyPanel)) {
$scope.showSpyPanel = true;
}
function getter(selector) {
return function () {
var $sel = $el.find(selector);
@ -36,8 +42,9 @@ define(function (require) {
var getVisEl = getter('.visualize-chart');
var getSpyEl = getter('visualize-spy');
$scope.spy = {mode: false};
$scope.fullScreenSpy = false;
$scope.spy = {};
$scope.spy.mode = ($scope.uiState) ? $scope.uiState.get('spy.mode', {}) : {};
var applyClassNames = function () {
var $spyEl = getSpyEl();
@ -76,7 +83,15 @@ define(function (require) {
};
}());
var loadingDelay = config.get('visualization:loadingDelay');
$scope.loadingStyle = {
'-webkit-transition-delay': loadingDelay,
'transition-delay': loadingDelay
};
// spy watchers
$scope.$watch('fullScreenSpy', applyClassNames);
$scope.$watchCollection('spy.mode', function (spyMode, oldSpyMode) {
var $visEl = getVisEl();
if (!$visEl) return;
@ -85,6 +100,7 @@ define(function (require) {
if (spyMode && !oldSpyMode) {
$scope.fullScreenSpy = $visEl.height() < minVisChartHeight;
}
applyClassNames();
});

View file

@ -15,6 +15,8 @@ visualize {
.visualize-chart {
flex: 1 1 auto;
overflow: auto;
-webkit-transition: opacity 0.01s;
transition: opacity 0.01s;
&.spy-visible {
margin-bottom: 10px;

View file

@ -6,7 +6,7 @@ module.exports = function (grunt) {
return {
options: {
branch: '2.0',
branch: '2.1',
fresh: !grunt.option('esvm-no-fresh'),
config: {
network: {