Merge branch 'master' of github.com:elastic/kibana into implement/testSubjectHelper
This commit is contained in:
commit
6f7b0377db
|
@ -19,9 +19,14 @@ You need at least one saved <<visualize, visualization>> to use a dashboard.
|
|||
|
||||
The first time you click the *Dashboard* tab, Kibana displays an empty dashboard.
|
||||
|
||||
image:images/NewDashboard.jpg[New Dashboard screen]
|
||||
image:images/NewDashboard.png[New Dashboard screen]
|
||||
|
||||
Build your dashboard by adding visualizations.
|
||||
Build your dashboard by adding visualizations. By default, Kibana dashboards use a light color theme. To use a dark color
|
||||
theme instead, click the *Settings* image:images/SettingsButton.jpg[Gear] button and check the *Use dark theme* box.
|
||||
|
||||
image:images/darktheme.png[Dark Theme Example]
|
||||
|
||||
NOTE: You can change the default theme in the *Advanced* section of the *Settings* tab.
|
||||
|
||||
[float]
|
||||
[[dash-autorefresh]]
|
||||
|
|
BIN
docs/images/NewDashboard.png
Normal file
BIN
docs/images/NewDashboard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
docs/images/darktheme.png
Normal file
BIN
docs/images/darktheme.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 196 KiB |
|
@ -100,7 +100,7 @@ class BaseOptimizer {
|
|||
test: /\.less$/,
|
||||
loader: ExtractTextPlugin.extract(
|
||||
'style',
|
||||
`css${mapQ}!autoprefixer?{ "browsers": ["last 2 versions","> 5%"] }!less${mapQ}`
|
||||
`css${mapQ}!autoprefixer${mapQ ? mapQ + '&' : '?'}{ "browsers": ["last 2 versions","> 5%"] }!less${mapQ}`
|
||||
)
|
||||
},
|
||||
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style', `css${mapQ}`) },
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
module.exports = function (kibana) {
|
||||
return new kibana.Plugin({
|
||||
uiExports: {
|
||||
app: {
|
||||
id: 'appSwitcher',
|
||||
main: 'plugins/appSwitcher/appSwitcher',
|
||||
hidden: true,
|
||||
autoload: kibana.autoload.styles
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "appSwitcher",
|
||||
"version": "1.0.0"
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
<div ng-if="switcher.loading">
|
||||
<div class="spinner large"></div>
|
||||
</div>
|
||||
<div ng-if="!switcher.loading" class="app-links">
|
||||
<div class="app-link" ng-repeat="app in switcher.apps | orderBy:'title'">
|
||||
<a ng-href="/app/{{app.id}}">
|
||||
|
||||
<div ng-if="app.icon" ng-style="{ 'background-image': 'url(../' + app.icon + ')' }" class="app-icon"></div>
|
||||
<div ng-if="!app.icon" class="app-icon app-icon-missing">{{app.title[0]}}</div>
|
||||
|
||||
<div class="app-title">{{ app.title }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -81,11 +81,16 @@ define(function (require) {
|
|||
var stateDefaults = {
|
||||
title: dash.title,
|
||||
panels: dash.panelsJSON ? JSON.parse(dash.panelsJSON) : [],
|
||||
options: dash.optionsJSON ? JSON.parse(dash.optionsJSON) : {},
|
||||
query: extractQueryFromFilters(dash.searchSource.getOwn('filter')) || {query_string: {query: '*'}},
|
||||
filters: _.reject(dash.searchSource.getOwn('filter'), matchQueryFilter)
|
||||
filters: _.reject(dash.searchSource.getOwn('filter'), matchQueryFilter),
|
||||
};
|
||||
|
||||
var $state = $scope.state = new AppState(stateDefaults);
|
||||
$scope.$watchCollection('state.options', function (newVal, oldVal) {
|
||||
if (!angular.equals(newVal, oldVal)) $state.save();
|
||||
});
|
||||
$scope.$watch('state.options.darkTheme', setDarkTheme);
|
||||
|
||||
$scope.configTemplate = new ConfigTemplate({
|
||||
save: require('plugins/kibana/dashboard/partials/save_dashboard.html'),
|
||||
|
@ -103,11 +108,6 @@ define(function (require) {
|
|||
|
||||
courier.setRootSearchSource(dash.searchSource);
|
||||
|
||||
setDarkTheme(dash.darkTheme);
|
||||
$scope.$watch('dash.darkTheme', function (value) {
|
||||
setDarkTheme(value);
|
||||
});
|
||||
|
||||
function init() {
|
||||
updateQueryOnRootSource();
|
||||
|
||||
|
@ -131,7 +131,7 @@ define(function (require) {
|
|||
}
|
||||
|
||||
function setDarkTheme(enabled) {
|
||||
var theme = !!enabled ? 'theme-dark' : 'theme-light';
|
||||
var theme = Boolean(enabled) ? 'theme-dark' : 'theme-light';
|
||||
chrome.removeApplicationClass(['theme-dark', 'theme-light']);
|
||||
chrome.addApplicationClass(theme);
|
||||
}
|
||||
|
@ -161,6 +161,7 @@ define(function (require) {
|
|||
dash.panelsJSON = angular.toJson($state.panels);
|
||||
dash.timeFrom = dash.timeRestore ? timefilter.time.from : undefined;
|
||||
dash.timeTo = dash.timeRestore ? timefilter.time.to : undefined;
|
||||
dash.optionsJSON = angular.toJson($state.options);
|
||||
|
||||
dash.save()
|
||||
.then(function (id) {
|
||||
|
@ -204,6 +205,7 @@ define(function (require) {
|
|||
// Setup configurable values for config directive, after objects are initialized
|
||||
$scope.opts = {
|
||||
dashboard: dash,
|
||||
ui: $state.options,
|
||||
save: $scope.save,
|
||||
addVis: $scope.addVis,
|
||||
addSearch: $scope.addSearch,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<p>
|
||||
<div class="input-group">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="opts.dashboard.darkTheme" ng-checked="opts.dashboard.darkTheme">
|
||||
<input type="checkbox" ng-model="opts.ui.darkTheme" ng-checked="opts.ui.darkTheme">
|
||||
Use dark theme
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
define(function (require) {
|
||||
var module = require('ui/modules').get('app/dashboard');
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
|
||||
|
@ -24,11 +25,13 @@ define(function (require) {
|
|||
hits: 0,
|
||||
description: '',
|
||||
panelsJSON: '[]',
|
||||
optionsJSON: angular.toJson({
|
||||
darkTheme: config.get('dashboard:defaultDarkTheme')
|
||||
}),
|
||||
version: 1,
|
||||
timeRestore: false,
|
||||
timeTo: undefined,
|
||||
timeFrom: undefined,
|
||||
darkTheme: config.get('dashboard:defaultDarkTheme')
|
||||
},
|
||||
|
||||
// if an indexPattern was saved with the searchsource of a SavedDashboard
|
||||
|
@ -46,11 +49,11 @@ define(function (require) {
|
|||
hits: 'integer',
|
||||
description: 'string',
|
||||
panelsJSON: 'string',
|
||||
optionsJSON: 'string',
|
||||
version: 'integer',
|
||||
timeRestore: 'boolean',
|
||||
timeTo: 'string',
|
||||
timeFrom: 'string',
|
||||
darkTheme: 'boolean'
|
||||
};
|
||||
|
||||
SavedDashboard.searchsource = true;
|
||||
|
|
|
@ -23,7 +23,7 @@ chrome
|
|||
.setNavBackground('#222222')
|
||||
.setTabDefaults({
|
||||
resetWhenActive: true,
|
||||
trackLastPath: true,
|
||||
lastUrlStore: window.sessionStore,
|
||||
activeIndicatorColor: '#656a76'
|
||||
})
|
||||
.setTabs([
|
||||
|
|
|
@ -5,6 +5,7 @@ module.exports = function (kibana) {
|
|||
title: 'Server Status',
|
||||
main: 'plugins/statusPage/statusPage',
|
||||
hidden: true,
|
||||
url: '/status',
|
||||
|
||||
autoload: [].concat(
|
||||
kibana.autoload.styles,
|
||||
|
@ -15,4 +16,3 @@ module.exports = function (kibana) {
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ class UiApp {
|
|||
this.hidden = this.spec.hidden;
|
||||
this.autoloadOverrides = this.spec.autoload;
|
||||
this.templateName = this.spec.templateName || 'uiApp';
|
||||
this.url = this.spec.url || '/app/' + this.id;
|
||||
|
||||
// once this resolves, no reason to run it again
|
||||
this.getModules = _.once(this.getModules);
|
||||
|
@ -39,7 +40,7 @@ class UiApp {
|
|||
}
|
||||
|
||||
toJSON() {
|
||||
return _.pick(this, ['id', 'title', 'description', 'icon', 'main']);
|
||||
return _.pick(this, ['id', 'title', 'description', 'icon', 'main', 'url']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,15 +40,6 @@ module.exports = async (kbnServer, server, config) => {
|
|||
server.setupViews(resolve(__dirname, 'views'));
|
||||
server.exposeStaticFile('/loading.gif', resolve(__dirname, 'public/loading.gif'));
|
||||
|
||||
// serve the app switcher
|
||||
server.route({
|
||||
path: '/api/apps',
|
||||
method: 'GET',
|
||||
handler: function (req, reply) {
|
||||
return reply(uiExports.apps);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
path: '/app/{id}',
|
||||
method: 'GET',
|
||||
|
@ -68,7 +59,7 @@ module.exports = async (kbnServer, server, config) => {
|
|||
server.decorate('reply', 'renderApp', function (app) {
|
||||
let payload = {
|
||||
app: app,
|
||||
appCount: uiExports.apps.size,
|
||||
nav: uiExports.apps,
|
||||
version: kbnServer.version,
|
||||
buildNum: config.get('pkg.buildNum'),
|
||||
buildSha: config.get('pkg.buildSha'),
|
||||
|
|
|
@ -1,37 +1,71 @@
|
|||
var _ = require('lodash');
|
||||
var storage = window.sessionStorage;
|
||||
const _ = require('lodash');
|
||||
const reEsc = require('lodash').escapeRegExp;
|
||||
const { parse, format } = require('url');
|
||||
|
||||
function Tab(spec) {
|
||||
this.id = spec.id;
|
||||
this.title = spec.title;
|
||||
this.active = false;
|
||||
this.resetWhenActive = !!spec.resetWhenActive;
|
||||
this.lastUrlStoreKey = spec.trackLastPath ? 'lastUrl:' + this.id : null;
|
||||
this.rootUrl = '/' + this.id;
|
||||
this.lastUrl = this.lastUrlStoreKey && storage.getItem(this.lastUrlStoreKey);
|
||||
const urlJoin = (a, b) => {
|
||||
if (!b) return a;
|
||||
return `${a}${ a.endsWith('/') ? '' : '/' }${b}`;
|
||||
};
|
||||
|
||||
this.activeIndicatorColor = spec.activeIndicatorColor || null;
|
||||
if (_.isFunction(this.activeIndicatorColor)) {
|
||||
// convert to a getter
|
||||
Object.defineProperty(this, 'activeIndicatorColor', {
|
||||
get: this.activeIndicatorColor
|
||||
});
|
||||
export default class Tab {
|
||||
constructor(spec = {}) {
|
||||
this.id = spec.id || '';
|
||||
this.title = spec.title || '';
|
||||
this.resetWhenActive = !!spec.resetWhenActive;
|
||||
this.activeIndicatorColor = spec.activeIndicatorColor || null;
|
||||
if (_.isFunction(this.activeIndicatorColor)) {
|
||||
// convert to a getter
|
||||
Object.defineProperty(this, 'activeIndicatorColor', {
|
||||
get: this.activeIndicatorColor
|
||||
});
|
||||
}
|
||||
|
||||
this.active = false;
|
||||
|
||||
this.baseUrl = spec.baseUrl || '/';
|
||||
this.rootUrl = urlJoin(this.baseUrl, this.id);
|
||||
this.rootRegExp = new RegExp(`^${reEsc(this.rootUrl)}(/|$|\\?|#)`);
|
||||
|
||||
this.lastUrlStoreKey = `lastUrl:${this.id}`;
|
||||
this.lastUrlStore = spec.lastUrlStore;
|
||||
this.lastUrl = this.lastUrlStore ? this.lastUrlStore.getItem(this.lastUrlStoreKey) : null;
|
||||
}
|
||||
|
||||
href() {
|
||||
if (this.active) {
|
||||
return this.resetWhenActive ? this.rootUrl : null;
|
||||
}
|
||||
return this.lastUrl || this.rootUrl;
|
||||
}
|
||||
|
||||
updateLastUrlGlobalState(globalState) {
|
||||
let lastPath = this.getLastPath();
|
||||
let { pathname, query, hash } = parse(lastPath, true);
|
||||
|
||||
query = query || {};
|
||||
if (!globalState) delete query._g;
|
||||
else query._g = globalState;
|
||||
|
||||
this.setLastUrl(`${this.rootUrl}${format({ pathname, query, hash })}`);
|
||||
}
|
||||
|
||||
getLastPath() {
|
||||
let { id, rootUrl } = this;
|
||||
let lastUrl = this.getLastUrl();
|
||||
|
||||
if (!lastUrl.startsWith(rootUrl)) {
|
||||
throw new Error(`Tab "${id}" has invalid root "${rootUrl}" for last url "${lastUrl}"`);
|
||||
}
|
||||
|
||||
return lastUrl.slice(rootUrl.length);
|
||||
}
|
||||
|
||||
setLastUrl(url) {
|
||||
this.lastUrl = url;
|
||||
if (this.lastUrlStore) this.lastUrlStore.setItem(this.lastUrlStoreKey, this.lastUrl);
|
||||
}
|
||||
|
||||
getLastUrl() {
|
||||
return this.lastUrl || this.rootUrl;
|
||||
}
|
||||
}
|
||||
|
||||
Tab.prototype.persistLastUrl = function (url) {
|
||||
if (!this.lastUrlStoreKey) return;
|
||||
this.lastUrl = url;
|
||||
storage.setItem(this.lastUrlStoreKey, this.lastUrl);
|
||||
};
|
||||
|
||||
Tab.prototype.href = function () {
|
||||
if (this.active) {
|
||||
if (this.resetWhenActive) return '#' + this.rootUrl;
|
||||
return null;
|
||||
}
|
||||
|
||||
return '#' + (this.lastUrl || this.rootUrl);
|
||||
};
|
||||
|
||||
module.exports = Tab;
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
var _ = require('lodash');
|
||||
var { startsWith, get, set, omit, wrap, pick } = require('lodash');
|
||||
var Tab = require('ui/chrome/Tab');
|
||||
var format = require('url').format;
|
||||
var parse = _.wrap(require('url').parse, function (parse, path) {
|
||||
var parsed = parse(path, true);
|
||||
return {
|
||||
pathname: parsed.pathname,
|
||||
query: parsed.query || {},
|
||||
hash: parsed.hash
|
||||
};
|
||||
});
|
||||
|
||||
function TabCollection() {
|
||||
var { parse } = require('url');
|
||||
|
||||
function TabCollection(opts = {}) {
|
||||
var tabs = [];
|
||||
var specs = null;
|
||||
var defaults = null;
|
||||
var defaults = opts.defaults || {};
|
||||
var activeTab = null;
|
||||
|
||||
this.set = function (_specs) {
|
||||
|
@ -23,7 +15,7 @@ function TabCollection() {
|
|||
};
|
||||
|
||||
this.setDefaults = function () {
|
||||
defaults = _.clone(arguments[0]);
|
||||
defaults = _.defaults({}, arguments[0], defaults);
|
||||
this._rebuildTabs();
|
||||
};
|
||||
|
||||
|
@ -42,20 +34,19 @@ function TabCollection() {
|
|||
return activeTab;
|
||||
};
|
||||
|
||||
this.consumeRouteUpdate = function ($location, persist) {
|
||||
var url = parse($location.url(), true);
|
||||
var id = $location.path().split('/')[1] || '';
|
||||
|
||||
this.consumeRouteUpdate = function (href, persist) {
|
||||
tabs.forEach(function (tab) {
|
||||
var active = tab.active = (tab.id === id);
|
||||
var lastUrl = active ? url : parse(tab.lastUrl || tab.rootUrl);
|
||||
lastUrl.query._g = url.query._g;
|
||||
|
||||
if (tab.active) activeTab = tab;
|
||||
if (persist) {
|
||||
tab.persistLastUrl(format(lastUrl));
|
||||
tab.active = tab.rootRegExp.test(href);
|
||||
if (tab.active) {
|
||||
activeTab = tab;
|
||||
activeTab.setLastUrl(href);
|
||||
}
|
||||
});
|
||||
|
||||
if (!persist || !activeTab) return;
|
||||
|
||||
let globalState = get(parse(activeTab.getLastPath(), true), 'query._g');
|
||||
tabs.forEach(tab => tab.updateLastUrlGlobalState(globalState));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
241
src/ui/public/chrome/__tests__/Tab.js
Normal file
241
src/ui/public/chrome/__tests__/Tab.js
Normal file
|
@ -0,0 +1,241 @@
|
|||
const Tab = require('../Tab');
|
||||
const expect = require('expect.js');
|
||||
const TabFakeStore = require('./_TabFakeStore');
|
||||
|
||||
describe('Chrome Tab', function () {
|
||||
describe('construction', function () {
|
||||
it('accepts id, title, resetWhenActive, trackLastUrl, activeIndicatorColor, baseUrl', function () {
|
||||
const tab = new Tab({
|
||||
id: 'foo',
|
||||
title: 'Foo App',
|
||||
resetWhenActive: false,
|
||||
activeIndicatorColor: true,
|
||||
baseUrl: 'proto:host.domain:999'
|
||||
});
|
||||
|
||||
expect(tab.id).to.equal('foo');
|
||||
expect(tab.title).to.equal('Foo App');
|
||||
expect(tab.resetWhenActive).to.equal(false);
|
||||
expect(tab.activeIndicatorColor).to.equal(true);
|
||||
expect(tab.rootUrl).to.equal('proto:host.domain:999/foo');
|
||||
|
||||
const tab2 = new Tab({
|
||||
id: 'bar',
|
||||
title: 'Bar App',
|
||||
resetWhenActive: true,
|
||||
activeIndicatorColor: false,
|
||||
baseUrl: 'proto:host.domain:999/sub/#/'
|
||||
});
|
||||
|
||||
expect(tab2.id).to.equal('bar');
|
||||
expect(tab2.title).to.equal('Bar App');
|
||||
expect(tab2.resetWhenActive).to.equal(true);
|
||||
expect(tab2.activeIndicatorColor).to.equal(null);
|
||||
expect(tab2.rootUrl).to.equal('proto:host.domain:999/sub/#/bar');
|
||||
});
|
||||
|
||||
it('starts inactive', function () {
|
||||
const tab = new Tab();
|
||||
expect(tab.active).to.equal(false);
|
||||
});
|
||||
|
||||
it('uses the id to set the rootUrl', function () {
|
||||
const id = 'foo';
|
||||
const tab = new Tab({ id });
|
||||
expect(tab.id).to.equal(id);
|
||||
expect(tab.rootUrl).to.equal(`/${id}`);
|
||||
});
|
||||
|
||||
it('creates a regexp for matching the rootUrl', function () {
|
||||
const tab = new Tab({ id: 'foo' });
|
||||
|
||||
expect('/foo').to.match(tab.rootRegExp);
|
||||
expect('/foo/bar').to.match(tab.rootRegExp);
|
||||
expect('/foo/bar/max').to.match(tab.rootRegExp);
|
||||
expect('/foo?bar=baz').to.match(tab.rootRegExp);
|
||||
expect('/foo/?bar=baz').to.match(tab.rootRegExp);
|
||||
expect('/foo#?bar=baz').to.match(tab.rootRegExp);
|
||||
|
||||
expect('/foobar').to.not.match(tab.rootRegExp);
|
||||
expect('site.com/foo#?bar=baz').to.not.match(tab.rootRegExp);
|
||||
expect('http://site.com/foo#?bar=baz').to.not.match(tab.rootRegExp);
|
||||
});
|
||||
|
||||
it('includes the baseUrl in the rootRegExp if specified', function () {
|
||||
const tab = new Tab({
|
||||
id: 'foo',
|
||||
baseUrl: 'http://spiderman.com/kibana'
|
||||
});
|
||||
|
||||
expect('http://spiderman.com/kibana/foo/bar').to.match(tab.rootRegExp);
|
||||
|
||||
expect('/foo').to.not.match(tab.rootRegExp);
|
||||
expect('https://spiderman.com/kibana/foo/bar').to.not.match(tab.rootRegExp);
|
||||
});
|
||||
|
||||
it('accepts a function for activeIndicatorColor', function () {
|
||||
let i = 0;
|
||||
const tab = new Tab({
|
||||
activeIndicatorColor: function () {
|
||||
return i++;
|
||||
}
|
||||
});
|
||||
expect(tab.activeIndicatorColor).to.equal(0);
|
||||
expect(tab.activeIndicatorColor).to.equal(1);
|
||||
expect(tab.activeIndicatorColor).to.equal(2);
|
||||
expect(tab.activeIndicatorColor).to.equal(3);
|
||||
});
|
||||
|
||||
it('discovers the lastUrl', function () {
|
||||
const lastUrlStore = new TabFakeStore();
|
||||
const tab = new Tab({ id: 'foo', lastUrlStore });
|
||||
expect(tab.lastUrl).to.not.equal('bar');
|
||||
|
||||
tab.setLastUrl('bar');
|
||||
expect(tab.lastUrl).to.equal('bar');
|
||||
|
||||
const tab2 = new Tab({ id: 'foo', lastUrlStore });
|
||||
expect(tab2.lastUrl).to.equal('bar');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#setLastUrl()', function () {
|
||||
it('updates the lastUrl and storage value if passed a lastUrlStore', function () {
|
||||
const lastUrlStore = new TabFakeStore();
|
||||
const tab = new Tab({ id: 'foo', lastUrlStore });
|
||||
|
||||
expect(tab.lastUrl).to.not.equal('foo');
|
||||
tab.setLastUrl('foo');
|
||||
expect(tab.lastUrl).to.equal('foo');
|
||||
expect(lastUrlStore.getItem(tab.lastUrlStoreKey)).to.equal('foo');
|
||||
});
|
||||
|
||||
it('only updates lastUrl if no lastUrlStore', function () {
|
||||
const tab = new Tab({ id: 'foo' });
|
||||
|
||||
expect(tab.lastUrl).to.equal(null);
|
||||
tab.setLastUrl('foo');
|
||||
expect(tab.lastUrl).to.equal('foo');
|
||||
|
||||
const tab2 = new Tab({ id: 'foo' });
|
||||
expect(tab2.lastUrl).to.not.equal('foo');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#href()', function () {
|
||||
it('returns the rootUrl/id be default', function () {
|
||||
const tab = new Tab({ id: 'foo' });
|
||||
expect(tab.href()).to.equal(tab.rootUrl);
|
||||
});
|
||||
|
||||
it('returns the lastUrl if tracking is on', function () {
|
||||
const tab = new Tab({ id: 'foo' });
|
||||
tab.setLastUrl('okay');
|
||||
expect(tab.href()).to.equal('okay');
|
||||
});
|
||||
|
||||
describe('when the tab is active', function () {
|
||||
it('returns the rootUrl when resetWhenActive: true', function () {
|
||||
const id = 'foo';
|
||||
const resetWhenActive = true;
|
||||
const tab = new Tab({ id, resetWhenActive });
|
||||
|
||||
tab.active = true;
|
||||
|
||||
expect(tab.href()).to.not.equal('butt');
|
||||
expect(tab.href()).to.equal(tab.rootUrl);
|
||||
});
|
||||
|
||||
it('or returns null when not', function () {
|
||||
const tab = new Tab({ id: 'foo', resetWhenActive: false });
|
||||
tab.active = true;
|
||||
|
||||
expect(tab.href()).to.not.equal('butt');
|
||||
expect(tab.href()).to.equal(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getLastPath()', function () {
|
||||
it('parses a path out of the lastUrl by removing the baseUrl', function () {
|
||||
const baseUrl = 'http://local:5601/app/visualize#';
|
||||
const tab = new Tab({ baseUrl });
|
||||
|
||||
tab.setLastUrl('http://local:5601/app/visualize#/index');
|
||||
expect(tab.getLastPath()).to.equal('/index');
|
||||
});
|
||||
|
||||
it('throws an error if the lastUrl does not extend the root url', function () {
|
||||
expect(function () {
|
||||
const baseUrl = 'http://local:5601/app/visualize#';
|
||||
const tab = new Tab({ baseUrl });
|
||||
|
||||
tab.setLastUrl('http://local:5601/');
|
||||
tab.getLastPath();
|
||||
}).to.throwError(/invalid.*root/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateLastUrlGlobalState', function () {
|
||||
const bases = [
|
||||
'http://local:5601',
|
||||
'',
|
||||
'weird.domain/with/subpath?path#',
|
||||
'weird.domain/with/#hashpath',
|
||||
];
|
||||
|
||||
context('with new state sets _g properly', function () {
|
||||
const paths = [
|
||||
[ '/', '/?_g=newState' ],
|
||||
[ '/?first', '/?first=&_g=newState' ],
|
||||
[ '/path?first=1&_g=afterHash', '/path?first=1&_g=newState' ],
|
||||
[ '/?first=1&_g=second', '/?first=1&_g=newState' ],
|
||||
[ '/?g=first', '/?g=first&_g=newState' ],
|
||||
[ '/a?first=1&_g=second', '/a?first=1&_g=newState' ],
|
||||
[ '/?first=1&_g=second', '/?first=1&_g=newState' ],
|
||||
[ '/?first&g=second', '/?first=&g=second&_g=newState' ],
|
||||
];
|
||||
|
||||
bases.forEach(baseUrl => {
|
||||
paths.forEach(([pathFrom, pathTo]) => {
|
||||
const fromUrl = `${baseUrl}${pathFrom}`;
|
||||
const toUrl = `${baseUrl}${pathTo}`;
|
||||
it(`${fromUrl} => ${toUrl}`, function () {
|
||||
const tab = new Tab({ baseUrl });
|
||||
tab.setLastUrl(fromUrl);
|
||||
tab.updateLastUrlGlobalState('newState');
|
||||
expect(tab.getLastUrl()).to.equal(toUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('with new empty state removes _g', function () {
|
||||
const paths = [
|
||||
[ '/', '/' ],
|
||||
[ '/?first', '/?first=' ],
|
||||
[ '/path?first=1&_g=afterHash', '/path?first=1' ],
|
||||
[ '/?first=1&_g=second', '/?first=1' ],
|
||||
[ '/?g=first', '/?g=first' ],
|
||||
[ '/a?first=1&_g=second', '/a?first=1' ],
|
||||
[ '/?first=1&_g=second', '/?first=1' ],
|
||||
[ '/?first&g=second', '/?first=&g=second' ],
|
||||
];
|
||||
|
||||
bases.forEach(baseUrl => {
|
||||
paths.forEach(([pathFrom, pathTo]) => {
|
||||
const fromUrl = `${baseUrl}${pathFrom}`;
|
||||
const toUrl = `${baseUrl}${pathTo}`;
|
||||
it(`${fromUrl}`, function () {
|
||||
const tab = new Tab({ baseUrl });
|
||||
tab.setLastUrl(fromUrl);
|
||||
tab.updateLastUrlGlobalState();
|
||||
expect(tab.getLastUrl()).to.equal(toUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
73
src/ui/public/chrome/__tests__/TabCollection.js
Normal file
73
src/ui/public/chrome/__tests__/TabCollection.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
const expect = require('expect.js');
|
||||
const { indexBy, random } = require('lodash');
|
||||
|
||||
const TabFakeStore = require('./_TabFakeStore');
|
||||
const TabCollection = require('../TabCollection');
|
||||
const Tab = require('../Tab');
|
||||
|
||||
describe('Chrome TabCollection', function () {
|
||||
describe('empty state', function () {
|
||||
it('has no tabs', function () {
|
||||
const tabs = new TabCollection();
|
||||
expect(tabs.get()).to.eql([]);
|
||||
});
|
||||
|
||||
it('has no active tab', function () {
|
||||
const tabs = new TabCollection();
|
||||
expect(!tabs.getActive()).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#set()', function () {
|
||||
it('consumes an ordered list of Tab specs', function () {
|
||||
const tabs = new TabCollection();
|
||||
tabs.set([
|
||||
{ id: 'foo' },
|
||||
{ id: 'bar' }
|
||||
]);
|
||||
|
||||
const ts = tabs.get();
|
||||
expect(ts.length).to.equal(2);
|
||||
expect(ts[0].id).to.equal('foo');
|
||||
expect(ts[1].id).to.equal('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setDefaults()', function () {
|
||||
it('applies the defaults used to create tabs', function () {
|
||||
const tabs = new TabCollection();
|
||||
tabs.setDefaults({ id: 'thing' });
|
||||
tabs.set([ {} ]);
|
||||
|
||||
expect(tabs.get()[0].id).to.equal('thing');
|
||||
});
|
||||
|
||||
it('recreates existing tabs with new defaults', function () {
|
||||
const tabs = new TabCollection();
|
||||
tabs.set([ {} ]);
|
||||
expect(!tabs.get()[0].id).to.equal(true);
|
||||
|
||||
tabs.setDefaults({ id: 'thing' });
|
||||
expect(tabs.get()[0].id).to.equal('thing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#consumeRouteUpdate()', function () {
|
||||
it('updates the active tab', function () {
|
||||
const store = new TabFakeStore();
|
||||
const baseUrl = `http://localhost:${random(1000, 9999)}`;
|
||||
const tabs = new TabCollection({ store, defaults: { baseUrl } });
|
||||
tabs.set([
|
||||
{ id: 'a' },
|
||||
{ id: 'b' }
|
||||
]);
|
||||
|
||||
tabs.consumeRouteUpdate(`${baseUrl}/a`);
|
||||
const {a, b} = indexBy(tabs.get(), 'id');
|
||||
expect(a.active).to.equal(true);
|
||||
expect(b.active).to.equal(false);
|
||||
expect(tabs.getActive()).to.equal(a);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
9
src/ui/public/chrome/__tests__/_TabFakeStore.js
Normal file
9
src/ui/public/chrome/__tests__/_TabFakeStore.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const store = Symbol('store');
|
||||
|
||||
export default class TabFakeStore {
|
||||
constructor() { this[store] = new Map(); }
|
||||
getItem(k) { return this[store].get(k); }
|
||||
setItem(k, v) { return this[store].set(k, v); }
|
||||
getKeys() { return [ ...this[store].keys() ]; }
|
||||
getValues() { return [ ...this[store].values() ]; }
|
||||
}
|
163
src/ui/public/chrome/api/__tests__/apps.js
Normal file
163
src/ui/public/chrome/api/__tests__/apps.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
const expect = require('expect.js');
|
||||
|
||||
const setup = require('../apps');
|
||||
const TabFakeStore = require('../../__tests__/_TabFakeStore');
|
||||
|
||||
describe('Chrome API :: apps', function () {
|
||||
describe('#get/setShowAppsLink()', function () {
|
||||
describe('defaults to false if there are less than two apps', function () {
|
||||
it('appCount = 0', function () {
|
||||
const chrome = {};
|
||||
setup(chrome, { nav: [] });
|
||||
expect(chrome.getShowAppsLink()).to.equal(false);
|
||||
});
|
||||
|
||||
it('appCount = 1', function () {
|
||||
const chrome = {};
|
||||
setup(chrome, { nav: [ { url: '/' } ] });
|
||||
expect(chrome.getShowAppsLink()).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaults to true if there are two or more apps', function () {
|
||||
it('appCount = 2', function () {
|
||||
const chrome = {};
|
||||
setup(chrome, { nav: [ { url: '/' }, { url: '/2' } ] });
|
||||
expect(chrome.getShowAppsLink()).to.equal(true);
|
||||
});
|
||||
|
||||
it('appCount = 3', function () {
|
||||
const chrome = {};
|
||||
setup(chrome, { nav: [ { url: '/' }, { url: '/2' }, { url: '/3' } ] });
|
||||
expect(chrome.getShowAppsLink()).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('is chainable', function () {
|
||||
const chrome = {};
|
||||
setup(chrome, { nav: [ { url: '/' } ] });
|
||||
expect(chrome.setShowAppsLink(true)).to.equal(chrome);
|
||||
});
|
||||
|
||||
it('can be changed', function () {
|
||||
const chrome = {};
|
||||
setup(chrome, { nav: [ { url: '/' } ] });
|
||||
|
||||
expect(chrome.setShowAppsLink(true).getShowAppsLink()).to.equal(true);
|
||||
expect(chrome.getShowAppsLink()).to.equal(true);
|
||||
|
||||
expect(chrome.setShowAppsLink(false).getShowAppsLink()).to.equal(false);
|
||||
expect(chrome.getShowAppsLink()).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getApp()', function () {
|
||||
it('returns a clone of the current app', function () {
|
||||
const chrome = {};
|
||||
const app = { url: '/' };
|
||||
setup(chrome, { app });
|
||||
|
||||
expect(chrome.getApp()).to.eql(app);
|
||||
expect(chrome.getApp()).to.not.equal(app);
|
||||
});
|
||||
|
||||
it('returns undefined if no active app', function () {
|
||||
const chrome = {};
|
||||
setup(chrome, {});
|
||||
expect(chrome.getApp()).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAppTitle()', function () {
|
||||
it('returns the title property of the current app', function () {
|
||||
const chrome = {};
|
||||
const app = { url: '/', title: 'foo' };
|
||||
setup(chrome, { app });
|
||||
expect(chrome.getAppTitle()).to.eql('foo');
|
||||
});
|
||||
|
||||
it('returns undefined if no active app', function () {
|
||||
const chrome = {};
|
||||
setup(chrome, {});
|
||||
expect(chrome.getAppTitle()).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAppUrl()', function () {
|
||||
it('returns the resolved url of the current app', function () {
|
||||
const chrome = {};
|
||||
const app = { url: '/foo' };
|
||||
setup(chrome, { app });
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('href', app.url);
|
||||
expect(chrome.getAppUrl()).to.equal(a.href);
|
||||
});
|
||||
|
||||
it('returns undefined if no active app', function () {
|
||||
const chrome = {};
|
||||
setup(chrome, {});
|
||||
expect(chrome.getAppUrl()).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getInjected()', function () {
|
||||
describe('called without args', function () {
|
||||
it('returns a clone of all injectedVars', function () {
|
||||
const chrome = {};
|
||||
const vars = { name: 'foo' };
|
||||
setup(chrome, { vars });
|
||||
expect(chrome.getInjected()).to.eql(vars);
|
||||
expect(chrome.getInjected()).to.not.equal(vars);
|
||||
});
|
||||
});
|
||||
|
||||
describe('called with a var name', function () {
|
||||
it('returns the var at that name', function () {
|
||||
const chrome = {};
|
||||
const vars = { name: 'foo' };
|
||||
setup(chrome, { vars });
|
||||
expect(chrome.getInjected('name')).to.equal('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('called with a var name and default', function () {
|
||||
it('returns the default when the var is undefined', function () {
|
||||
const chrome = {};
|
||||
const vars = { name: undefined };
|
||||
setup(chrome, { vars });
|
||||
expect(chrome.getInjected('name', 'bar')).to.equal('bar');
|
||||
});
|
||||
|
||||
it('returns null when the var is null', function () {
|
||||
const chrome = {};
|
||||
const vars = { name: null };
|
||||
setup(chrome, { vars });
|
||||
expect(chrome.getInjected('name', 'bar')).to.equal(null);
|
||||
});
|
||||
|
||||
it('returns var if not undefined', function () {
|
||||
const chrome = {};
|
||||
const vars = { name: 'kim' };
|
||||
setup(chrome, { vars });
|
||||
expect(chrome.getInjected('name', 'bar')).to.equal('kim');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get/setLastUrlFor()', function () {
|
||||
it('reads/writes last url from storage', function () {
|
||||
const chrome = {};
|
||||
const store = new TabFakeStore();
|
||||
setup(chrome, { appUrlStore: store });
|
||||
expect(chrome.getLastUrlFor('app')).to.equal(undefined);
|
||||
chrome.setLastUrlFor('app', 'url');
|
||||
expect(chrome.getLastUrlFor('app')).to.equal('url');
|
||||
expect(store.getKeys().length).to.equal(1);
|
||||
expect(store.getValues().shift()).to.equal('url');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
17
src/ui/public/chrome/api/angular.js
vendored
17
src/ui/public/chrome/api/angular.js
vendored
|
@ -1,7 +1,11 @@
|
|||
var modules = require('ui/modules');
|
||||
var $ = require('jquery');
|
||||
var _ = require('lodash');
|
||||
|
||||
require('../appSwitcher');
|
||||
var modules = require('ui/modules');
|
||||
var ConfigTemplate = require('ui/ConfigTemplate');
|
||||
require('ui/directives/config');
|
||||
|
||||
module.exports = function (chrome, internals) {
|
||||
chrome.setupAngular = function () {
|
||||
var kibana = modules.get('kibana');
|
||||
|
@ -44,7 +48,13 @@ module.exports = function (chrome, internals) {
|
|||
chrome.setVisible(!Boolean($location.search().embed));
|
||||
|
||||
// listen for route changes, propogate to tabs
|
||||
var onRouteChange = _.bindKey(internals.tabs, 'consumeRouteUpdate', $location, chrome.getVisible());
|
||||
var onRouteChange = function () {
|
||||
let { href } = window.location;
|
||||
let persist = chrome.getVisible();
|
||||
internals.trackPossibleSubUrl(href);
|
||||
internals.tabs.consumeRouteUpdate(href, persist);
|
||||
};
|
||||
|
||||
$rootScope.$on('$routeChangeSuccess', onRouteChange);
|
||||
$rootScope.$on('$routeUpdate', onRouteChange);
|
||||
onRouteChange();
|
||||
|
@ -52,6 +62,9 @@ module.exports = function (chrome, internals) {
|
|||
// and some local values
|
||||
$scope.httpActive = $http.pendingRequests;
|
||||
$scope.notifList = require('ui/notify')._notifs;
|
||||
$scope.appSwitcherTemplate = new ConfigTemplate({
|
||||
switcher: '<app-switcher></app-switcher>'
|
||||
});
|
||||
|
||||
return chrome;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
var _ = require('lodash');
|
||||
const { clone, get } = require('lodash');
|
||||
const { resolve } = require('url');
|
||||
|
||||
module.exports = function (chrome, internals) {
|
||||
|
||||
if (internals.app) {
|
||||
internals.app.url = resolve(window.location.href, internals.app.url);
|
||||
}
|
||||
|
||||
internals.appUrlStore = internals.appUrlStore || window.sessionStorage;
|
||||
|
||||
/**
|
||||
* ui/chrome apps API
|
||||
*
|
||||
|
@ -16,24 +23,33 @@ module.exports = function (chrome, internals) {
|
|||
};
|
||||
|
||||
chrome.getShowAppsLink = function () {
|
||||
return internals.showAppsLink == null ? internals.appCount > 1 : internals.showAppsLink;
|
||||
return internals.showAppsLink == null ? internals.nav.length > 1 : internals.showAppsLink;
|
||||
};
|
||||
|
||||
chrome.getApp = function () {
|
||||
return _.clone(internals.app);
|
||||
return clone(internals.app);
|
||||
};
|
||||
|
||||
chrome.getAppTitle = function () {
|
||||
return internals.app.title;
|
||||
return get(internals, ['app', 'title']);
|
||||
};
|
||||
|
||||
chrome.getAppId = function () {
|
||||
return internals.app.id;
|
||||
chrome.getAppUrl = function () {
|
||||
return get(internals, ['app', 'url']);
|
||||
};
|
||||
|
||||
chrome.getInjected = function (name, def) {
|
||||
if (name == null) return _.clone(internals.vars) || {};
|
||||
return _.get(internals.vars, name, def);
|
||||
if (name == null) return clone(internals.vars) || {};
|
||||
return get(internals.vars, name, def);
|
||||
};
|
||||
|
||||
chrome.getLastUrlFor = function (appId) {
|
||||
return internals.appUrlStore.getItem(`appLastUrl:${appId}`);
|
||||
};
|
||||
|
||||
chrome.setLastUrlFor = function (appId, url) {
|
||||
internals.appUrlStore.setItem(`appLastUrl:${appId}`, url);
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
|
|
34
src/ui/public/chrome/api/nav.js
Normal file
34
src/ui/public/chrome/api/nav.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
module.exports = function (chrome, internals) {
|
||||
const { startsWith } = require('lodash');
|
||||
|
||||
chrome.getNavLinks = function () {
|
||||
return internals.nav;
|
||||
};
|
||||
|
||||
chrome.getLastSubUrlFor = function (url) {
|
||||
return internals.appUrlStore.getItem(`lastSubUrl:${url}`);
|
||||
};
|
||||
|
||||
internals.trackPossibleSubUrl = function (url) {
|
||||
for (const link of internals.nav) {
|
||||
if (startsWith(url, link.url)) {
|
||||
link.lastSubUrl = url;
|
||||
internals.appUrlStore.setItem(`lastSubUrl:${link.url}`, url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
internals.nav.forEach(link => {
|
||||
// convert all link urls to absolute urls
|
||||
|
||||
var a = document.createElement('a');
|
||||
a.setAttribute('href', link.url);
|
||||
link.url = a.href;
|
||||
link.lastSubUrl = chrome.getLastSubUrlFor(link.url);
|
||||
|
||||
if (link.url === chrome.getAppUrl()) {
|
||||
link.active = true;
|
||||
}
|
||||
});
|
||||
|
||||
};
|
|
@ -1,7 +1,14 @@
|
|||
var _ = require('lodash');
|
||||
var TabCollection = require('../TabCollection');
|
||||
|
||||
module.exports = function (chrome, internals) {
|
||||
|
||||
internals.tabs = new TabCollection({
|
||||
defaults: {
|
||||
baseUrl: `${chrome.getAppUrl()}#/`
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ui/chrome tabs API
|
||||
*
|
||||
|
@ -26,7 +33,7 @@ module.exports = function (chrome, internals) {
|
|||
* when the the tab is considered active, should clicking it
|
||||
* cause a redirect to just the id?
|
||||
*
|
||||
* trackLastPath {boolean}
|
||||
* trackLastUrl {boolean}
|
||||
* When this tab is active, should the current path be tracked
|
||||
* and persisted to session storage, then used as the tabs href attribute when the user navigates
|
||||
* away from the tab?
|
||||
|
|
221
src/ui/public/chrome/appSwitcher/__tests__/appSwitcher.js
Normal file
221
src/ui/public/chrome/appSwitcher/__tests__/appSwitcher.js
Normal file
|
@ -0,0 +1,221 @@
|
|||
var sinon = require('auto-release-sinon');
|
||||
var ngMock = require('ngMock');
|
||||
var $ = require('jquery');
|
||||
var expect = require('expect.js');
|
||||
var constant = require('lodash').constant;
|
||||
var set = require('lodash').set;
|
||||
var cloneDeep = require('lodash').cloneDeep;
|
||||
var indexBy = require('lodash').indexBy;
|
||||
|
||||
require('ui/chrome');
|
||||
require('ui/chrome/appSwitcher');
|
||||
var DomLocationProvider = require('ui/domLocation');
|
||||
|
||||
describe('appSwitcher directive', function () {
|
||||
var env;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
|
||||
function setup(href, links) {
|
||||
return ngMock.inject(function ($window, $rootScope, $compile, Private) {
|
||||
var domLocation = Private(DomLocationProvider);
|
||||
|
||||
$rootScope.chrome = {
|
||||
getNavLinks: constant(cloneDeep(links)),
|
||||
};
|
||||
|
||||
env = {
|
||||
$scope: $rootScope,
|
||||
$el: $compile($('<app-switcher>'))($rootScope),
|
||||
currentHref: href,
|
||||
location: domLocation
|
||||
};
|
||||
|
||||
Object.defineProperties(domLocation, {
|
||||
href: {
|
||||
get: function () { return env.currentHref; },
|
||||
set: function (val) { return env.currentHref = val; },
|
||||
},
|
||||
reload: {
|
||||
value: sinon.stub()
|
||||
}
|
||||
});
|
||||
|
||||
env.$scope.$digest();
|
||||
});
|
||||
}
|
||||
|
||||
context('when one link is for the active app', function () {
|
||||
var myLink = {
|
||||
active: true,
|
||||
title: 'myLink',
|
||||
url: 'http://localhost:555/app/myApp',
|
||||
lastSubUrl: 'http://localhost:555/app/myApp#/lastSubUrl'
|
||||
};
|
||||
|
||||
var notMyLink = {
|
||||
active: false,
|
||||
title: 'notMyLink',
|
||||
url: 'http://localhost:555/app/notMyApp',
|
||||
lastSubUrl: 'http://localhost:555/app/notMyApp#/lastSubUrl'
|
||||
};
|
||||
|
||||
beforeEach(setup('http://localhost:5555/app/myApp/', [myLink, notMyLink]));
|
||||
|
||||
it('links to the inactive apps base url', function () {
|
||||
var $myLink = env.$el.findTestSubject('appLink').eq(0);
|
||||
expect($myLink.prop('href')).to.be(myLink.url);
|
||||
expect($myLink.prop('href')).to.not.be(myLink.lastSubUrl);
|
||||
});
|
||||
|
||||
it('links to the inactive apps last sub url', function () {
|
||||
var $notMyLink = env.$el.findTestSubject('appLink').eq(1);
|
||||
expect($notMyLink.prop('href')).to.be(notMyLink.lastSubUrl);
|
||||
expect($notMyLink.prop('href')).to.not.be(notMyLink.url);
|
||||
});
|
||||
});
|
||||
|
||||
context('when none of the links are for the active app', function () {
|
||||
var myLink = {
|
||||
active: false,
|
||||
title: 'myLink',
|
||||
url: 'http://localhost:555/app/myApp',
|
||||
lastSubUrl: 'http://localhost:555/app/myApp#/lastSubUrl'
|
||||
};
|
||||
|
||||
var notMyLink = {
|
||||
active: false,
|
||||
title: 'notMyLink',
|
||||
url: 'http://localhost:555/app/notMyApp',
|
||||
lastSubUrl: 'http://localhost:555/app/notMyApp#/lastSubUrl'
|
||||
};
|
||||
|
||||
beforeEach(setup('http://localhost:5555/app/myApp/', [myLink, notMyLink]));
|
||||
|
||||
it('links to the lastSubUrl for each', function () {
|
||||
var $links = env.$el.findTestSubject('appLink');
|
||||
var $myLink = $links.eq(0);
|
||||
var $notMyLink = $links.eq(1);
|
||||
|
||||
expect($myLink.prop('href')).to.be(myLink.lastSubUrl);
|
||||
expect($myLink.prop('href')).to.not.be(myLink.url);
|
||||
|
||||
expect($notMyLink.prop('href')).to.be(notMyLink.lastSubUrl);
|
||||
expect($notMyLink.prop('href')).to.not.be(notMyLink.url);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link with matching href but missing hash', function () {
|
||||
var url = 'http://localhost:555/app/myApp?query=1';
|
||||
beforeEach(setup(url + '#/lastSubUrl', [
|
||||
{ url: url }
|
||||
]));
|
||||
|
||||
it('just prevents propogation (no reload)', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
var $link = env.$el.findTestSubject('appLink');
|
||||
expect($link.prop('href')).to.be(url);
|
||||
$link.trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link that matches entire url', function () {
|
||||
var url = 'http://localhost:555/app/myApp#/lastSubUrl';
|
||||
beforeEach(setup(url, [
|
||||
{ url: url }
|
||||
]));
|
||||
|
||||
it('calls window.location.reload and prevents propogation', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
var $link = env.$el.findTestSubject('appLink');
|
||||
expect($link.prop('href')).to.be(env.currentHref);
|
||||
$link.trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(1);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link with matching href but changed hash', function () {
|
||||
var rootUrl = 'http://localhost:555/app/myApp?query=1';
|
||||
var url = rootUrl + '#/lastSubUrl2';
|
||||
|
||||
beforeEach(setup(url + '#/lastSubUrl', [
|
||||
{ url: url }
|
||||
]));
|
||||
|
||||
it('calls window.location.reload and prevents propogation', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
var $link = env.$el.findTestSubject('appLink');
|
||||
expect($link.prop('href')).to.be(url);
|
||||
$link.trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(1);
|
||||
expect(event.isDefaultPrevented()).to.be(false);
|
||||
expect(event.isPropagationStopped()).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link with matching host', function () {
|
||||
beforeEach(setup('http://localhost:555/someOtherPath', [
|
||||
{
|
||||
active: true,
|
||||
url: 'http://localhost:555/app/myApp'
|
||||
}
|
||||
]));
|
||||
|
||||
it('allows click through', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
env.$el.findTestSubject('appLink').trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
context('clicking a link with matching host and path', function () {
|
||||
beforeEach(setup('http://localhost:555/app/myApp?someQuery=true', [
|
||||
{
|
||||
active: true,
|
||||
url: 'http://localhost:555/app/myApp?differentQuery=true'
|
||||
}
|
||||
]));
|
||||
|
||||
it('allows click through', function () {
|
||||
var event = new $.Event('click');
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
|
||||
env.$el.findTestSubject('appLink').trigger(event);
|
||||
|
||||
expect(env.location.reload.callCount).to.be(0);
|
||||
expect(event.isPropagationStopped()).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
18
src/ui/public/chrome/appSwitcher/appSwitcher.html
Normal file
18
src/ui/public/chrome/appSwitcher/appSwitcher.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="app-links">
|
||||
<div
|
||||
class="app-link"
|
||||
ng-repeat="link in switcher.getNavLinks() | orderBy:'title'"
|
||||
ng-class="{ active: link.active }">
|
||||
|
||||
<a
|
||||
ng-click="switcher.ensureNavigation($event, link)"
|
||||
ng-href="{{ link.active ? link.url : (link.lastSubUrl || link.url) }}"
|
||||
data-test-subj="appLink">
|
||||
|
||||
<div ng-if="link.icon" ng-style="{ 'background-image': 'url(../' + link.icon + ')' }" class="app-icon"></div>
|
||||
<div ng-if="!link.icon" class="app-icon app-icon-missing">{{ link.title[0] }}</div>
|
||||
|
||||
<div class="app-title">{{ link.title }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
51
src/ui/public/chrome/appSwitcher/appSwitcher.js
Normal file
51
src/ui/public/chrome/appSwitcher/appSwitcher.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
var parse = require('url').parse;
|
||||
var bindKey = require('lodash').bindKey;
|
||||
|
||||
require('../appSwitcher/appSwitcher.less');
|
||||
var DomLocationProvider = require('ui/domLocation');
|
||||
|
||||
require('ui/modules')
|
||||
.get('kibana')
|
||||
.directive('appSwitcher', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: require('./appSwitcher.html'),
|
||||
controllerAs: 'switcher',
|
||||
controller: function ($scope, Private) {
|
||||
var domLocation = Private(DomLocationProvider);
|
||||
|
||||
// since we render this in an isolate scope we can't "require: ^chrome", but
|
||||
// rather than remove all helpfull checks we can just check here.
|
||||
if (!$scope.chrome || !$scope.chrome.getNavLinks) {
|
||||
throw new TypeError('appSwitcher directive requires the "chrome" config-object');
|
||||
}
|
||||
|
||||
this.getNavLinks = bindKey($scope.chrome, 'getNavLinks');
|
||||
|
||||
// links don't cause full-navigation events in certain scenarios
|
||||
// so we force them when needed
|
||||
this.ensureNavigation = function (event, app) {
|
||||
if (event.isDefaultPrevented() || event.altKey || event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
var toParsed = parse(event.delegateTarget.href);
|
||||
var fromParsed = parse(domLocation.href);
|
||||
var sameProto = toParsed.protocol === fromParsed.protocol;
|
||||
var sameHost = toParsed.host === fromParsed.host;
|
||||
var samePath = toParsed.path === fromParsed.path;
|
||||
|
||||
if (sameProto && sameHost && samePath) {
|
||||
toParsed.hash && domLocation.reload();
|
||||
|
||||
// event.preventDefault() keeps the browser from seeing the new url as an update
|
||||
// and even setting window.location does not mimic that behavior, so instead
|
||||
// we use stopPropagation() to prevent angular from seeing the click and
|
||||
// starting a digest cycle/attempting to handle it in the router.
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
@import (reference) "~ui/styles/variables";
|
||||
|
||||
@app-icon-size: 48px;
|
||||
@app-icon-padding: 10px;
|
||||
|
||||
.app-links {
|
||||
text-align: justify;
|
||||
|
@ -8,9 +9,11 @@
|
|||
.app-link {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: @app-icon-size;
|
||||
margin: 0px 10px;
|
||||
text-align: left;
|
||||
width: @app-icon-size + (@app-icon-padding * 2);
|
||||
margin: 0px 10px;
|
||||
padding: @app-icon-padding;
|
||||
border-radius: @border-radius-base;
|
||||
|
||||
.app-icon {
|
||||
display: block;
|
||||
|
@ -43,6 +46,13 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: @gray-lighter;
|
||||
.app-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -47,9 +47,9 @@
|
|||
</a>
|
||||
</li>
|
||||
|
||||
<li class="to-body" ng-class="{active: appTemplate.is('switcher')}" ng-if="chrome.getShowAppsLink()">
|
||||
<a ng-click="appTemplate.toggle('switcher')">
|
||||
<i class="fa fa-th" alt="Go to app switcher"></i>
|
||||
<li class="to-body" ng-class="{ active: appSwitcherTemplate.is('switcher') }" ng-if="chrome.getShowAppsLink()">
|
||||
<a ng-click="appSwitcherTemplate.toggle('switcher')">
|
||||
<i class="fa fa-th" alt="Show app switcher"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -98,9 +98,9 @@
|
|||
</nav>
|
||||
|
||||
<config
|
||||
config-template="appTemplate"
|
||||
config-object="switcher"
|
||||
config-close="appTemplate.close">
|
||||
config-template="appSwitcherTemplate"
|
||||
config-object="chrome"
|
||||
config-close="appSwitcherTemplate.close">
|
||||
</config>
|
||||
|
||||
<config
|
||||
|
|
|
@ -11,20 +11,15 @@ require('ui/promises');
|
|||
var metadata = require('ui/metadata');
|
||||
var TabCollection = require('ui/chrome/TabCollection');
|
||||
|
||||
var chrome = {
|
||||
navBackground: '#222222',
|
||||
logo: null,
|
||||
smallLogo: null
|
||||
};
|
||||
|
||||
var internals = _.assign(
|
||||
var chrome = {};
|
||||
var internals = _.defaults(
|
||||
_.cloneDeep(metadata),
|
||||
{
|
||||
tabs: new TabCollection(),
|
||||
rootController: null,
|
||||
rootTemplate: null,
|
||||
showAppsLink: null,
|
||||
brand: null,
|
||||
nav: [],
|
||||
applicationClasses: []
|
||||
}
|
||||
);
|
||||
|
@ -35,6 +30,7 @@ $('<link>').attr({
|
|||
}).appendTo('head');
|
||||
|
||||
require('./api/apps')(chrome, internals);
|
||||
require('./api/nav')(chrome, internals);
|
||||
require('./api/angular')(chrome, internals);
|
||||
require('./api/controls')(chrome, internals);
|
||||
require('./api/tabs')(chrome, internals);
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
define(function (require) {
|
||||
require('plugins/appSwitcher/appSwitcher.less');
|
||||
|
||||
var _ = require('lodash');
|
||||
var ConfigTemplate = require('ui/ConfigTemplate');
|
||||
|
||||
require('ui/modules')
|
||||
.get('kibana')
|
||||
.directive('chromeContext', function (timefilter, globalState, $http) {
|
||||
.directive('chromeContext', function (timefilter, globalState) {
|
||||
|
||||
var listenForUpdates = _.once(function ($scope) {
|
||||
$scope.$listen(timefilter, 'update', function (newVal, oldVal) {
|
||||
|
@ -27,17 +25,6 @@ define(function (require) {
|
|||
interval: require('ui/chrome/config/interval.html')
|
||||
});
|
||||
|
||||
|
||||
$scope.switcher = {loading: true};
|
||||
$http.get('/api/apps')
|
||||
.then(function (resp) {
|
||||
$scope.switcher.loading = false;
|
||||
$scope.switcher.apps = resp.data;
|
||||
});
|
||||
$scope.appTemplate = new ConfigTemplate({
|
||||
switcher: require('plugins/appSwitcher/appSwitcher.html')
|
||||
});
|
||||
|
||||
$scope.toggleRefresh = function () {
|
||||
timefilter.refreshInterval.pause = !timefilter.refreshInterval.pause;
|
||||
};
|
||||
|
|
|
@ -54,7 +54,7 @@ define(function () {
|
|||
value: 30,
|
||||
description: 'Requests in discover are split into segments to prevent massive requests from being sent to ' +
|
||||
'elasticsearch. This setting attempts to prevent the list of segments from getting too long, which might ' +
|
||||
'cause requests to take much longer to process.'
|
||||
'cause requests to take much longer to process'
|
||||
},
|
||||
'fields:popularLimit': {
|
||||
value: 10,
|
||||
|
@ -74,7 +74,7 @@ define(function () {
|
|||
'12 is the max. ' +
|
||||
'<a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/' +
|
||||
'search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator" target="_blank">' +
|
||||
'Explanation of cell dimensions.</a>',
|
||||
'Explanation of cell dimensions</a>',
|
||||
},
|
||||
'visualization:tileMap:WMSdefaults': {
|
||||
value: JSON.stringify({
|
||||
|
@ -117,12 +117,12 @@ define(function () {
|
|||
},
|
||||
'truncate:maxHeight': {
|
||||
value: 115,
|
||||
description: 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation.'
|
||||
description: 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation'
|
||||
},
|
||||
'indexPattern:fieldMapping:lookBack': {
|
||||
value: 5,
|
||||
description: 'For index patterns containing timestamps in their names, look for this many recent matching ' +
|
||||
'patterns from which to query the field mapping.'
|
||||
'patterns from which to query the field mapping'
|
||||
},
|
||||
'format:defaultTypeMap': {
|
||||
type: 'json',
|
||||
|
@ -136,23 +136,27 @@ define(function () {
|
|||
'}',
|
||||
].join('\n'),
|
||||
description: 'Map of the format name to use by default for each field type. ' +
|
||||
'"_default_" is used if the field type is not mentioned explicitly.'
|
||||
'"_default_" is used if the field type is not mentioned explicitly'
|
||||
},
|
||||
'format:number:defaultPattern': {
|
||||
type: 'string',
|
||||
value: '0,0.[000]'
|
||||
value: '0,0.[000]',
|
||||
description: 'Default numeral format for the "number" format'
|
||||
},
|
||||
'format:bytes:defaultPattern': {
|
||||
type: 'string',
|
||||
value: '0,0.[000]b'
|
||||
value: '0,0.[000]b',
|
||||
description: 'Default numeral format for the "bytes" format'
|
||||
},
|
||||
'format:percent:defaultPattern': {
|
||||
type: 'string',
|
||||
value: '0,0.[000]%'
|
||||
value: '0,0.[000]%',
|
||||
description: 'Default numeral format for the "percent" format'
|
||||
},
|
||||
'format:currency:defaultPattern': {
|
||||
type: 'string',
|
||||
value: '($0,0.[00])'
|
||||
value: '($0,0.[00])',
|
||||
description: 'Default numeral format for the "currency" format'
|
||||
},
|
||||
'timepicker:timeDefaults': {
|
||||
type: 'json',
|
||||
|
@ -162,7 +166,8 @@ define(function () {
|
|||
' "to": "now",',
|
||||
' "mode": "quick"',
|
||||
'}'
|
||||
].join('\n')
|
||||
].join('\n'),
|
||||
description: 'The timefilter selection to use when Kibana is started without one'
|
||||
},
|
||||
'timepicker:refreshIntervalDefaults': {
|
||||
type: 'json',
|
||||
|
@ -172,7 +177,8 @@ define(function () {
|
|||
' "pause": false,',
|
||||
' "value": 0',
|
||||
'}'
|
||||
].join('\n')
|
||||
].join('\n'),
|
||||
description: 'The timefilter\'s default refresh interval'
|
||||
},
|
||||
'dashboard:defaultDarkTheme': {
|
||||
value: false,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<div class="spinner large" ng-show="searchSource.activeFetchCount > 0"></div>
|
||||
<div
|
||||
ng-if="hits.length"
|
||||
ng-class="{ loading: searchSource.activeFetchCount > 0 }">
|
||||
|
|
15
src/ui/public/domLocation.js
Normal file
15
src/ui/public/domLocation.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
module.exports = function DomLocationProvider($window) {
|
||||
return {
|
||||
reload: function (forceFetch) {
|
||||
$window.location.reload(forceFetch);
|
||||
},
|
||||
|
||||
get href() {
|
||||
return $window.location.href;
|
||||
},
|
||||
|
||||
set href(val) {
|
||||
return ($window.location.href = val);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -111,6 +111,22 @@ describe('Filter Manager', function () {
|
|||
filterManager.add('_exists_', 'myField', '-', 'myIndex');
|
||||
checkAddFilters(0, null, 3);
|
||||
expect(appState.filters).to.have.length(2);
|
||||
|
||||
var scriptedField = {name: 'scriptedField', scripted: true, script: 1};
|
||||
filterManager.add(scriptedField, 1, '+', 'myIndex');
|
||||
checkAddFilters(1, [{
|
||||
meta: {index: 'myIndex', negate: false, field: 'scriptedField'},
|
||||
script: {
|
||||
script: '(' + scriptedField.script + ') == value',
|
||||
lang: scriptedField.lang,
|
||||
params: {value: 1}
|
||||
}
|
||||
}], 4);
|
||||
expect(appState.filters).to.have.length(3);
|
||||
|
||||
filterManager.add(scriptedField, 1, '-', 'myIndex');
|
||||
checkAddFilters(0, null, 5);
|
||||
expect(appState.filters).to.have.length(3);
|
||||
});
|
||||
|
||||
it('should enable matching filters being changed', function () {
|
||||
|
|
|
@ -26,6 +26,10 @@ define(function (require) {
|
|||
if (filter.query) {
|
||||
return filter.query.match[fieldName] && filter.query.match[fieldName].query === value;
|
||||
}
|
||||
|
||||
if (filter.script) {
|
||||
return filter.meta.field === fieldName && filter.script.params.value === value;
|
||||
}
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<div class="spinner large" ng-show="vis.type.requiresSearch && searchSource.activeFetchCount > 0"></div>
|
||||
<div ng-if="vis.type.requiresSearch && esResp.hits.total === 0"
|
||||
class="text-center visualize-error visualize-chart">
|
||||
<div class="item top"></div>
|
||||
|
|
3
webpackShims/jquery.js
vendored
3
webpackShims/jquery.js
vendored
|
@ -1,2 +1 @@
|
|||
var $ = window.jQuery = window.$ = module.exports = require('node_modules/jquery/dist/jquery');
|
||||
require('ui/jquery/findTestSubject')($);
|
||||
window.jQuery = window.$ = module.exports = require('node_modules/jquery/dist/jquery');
|
||||
|
|
Loading…
Reference in a new issue